Release process (#3909)

This commit is contained in:
Michał Wawrzyniec Urbańczyk 2022-12-02 02:56:22 +01:00 committed by GitHub
parent d9cdb32121
commit 4de8e44871
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 2258 additions and 1444 deletions

View File

@ -4,742 +4,11 @@ on:
- cron: 0 5 * * 2-6
workflow_dispatch: {}
jobs:
enso-build-cli-ci-gen-draft-release-linux:
name: Create release draft
runs-on:
- self-hosted
- Linux
- engine
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- id: prepare
run: ./run release create-draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
outputs:
ENSO_RELEASE_ID: ${{ steps.prepare.outputs.ENSO_RELEASE_ID }}
ENSO_VERSION: ${{ steps.prepare.outputs.ENSO_VERSION }}
enso-build-cli-ci-gen-job-build-wasm-linux:
name: Build GUI (WASM) (linux)
runs-on:
- self-hosted
- Linux
- engine
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- run: ./run --upload-artifacts ${{ runner.os == 'Linux' }} wasm build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "always() && always() && contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
enso-build-cli-ci-gen-job-deploy-gui-linux:
name: Upload GUI to S3 (linux)
needs:
- enso-build-cli-ci-gen-upload-ide-linux
runs-on:
- self-hosted
- Linux
- engine
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- run: ./run release deploy-gui
env:
AWS_ACCESS_KEY_ID: ${{ secrets.ARTEFACT_S3_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.ARTEFACT_S3_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.CI_PRIVATE_TOKEN }}
timeout-minutes: 360
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "always() && always() && contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
enso-build-cli-ci-gen-job-deploy-runtime-linux:
name: Upload Runtime to ECR (linux)
needs:
- enso-build-cli-ci-gen-draft-release-linux
- enso-build-cli-ci-gen-job-upload-backend-linux
runs-on:
- self-hosted
- Linux
- engine
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- run: ./run release deploy-runtime
env:
AWS_ACCESS_KEY_ID: ${{ secrets.ECR_PUSH_RUNTIME_ACCESS_KEY_ID }}
AWS_DEFAULT_REGION: eu-west-1
AWS_SECRET_ACCESS_KEY: ${{ secrets.ECR_PUSH_RUNTIME_SECRET_ACCESS_KEY }}
ENSO_BUILD_ECR_REPOSITORY: runtime
GITHUB_TOKEN: ${{ secrets.CI_PRIVATE_TOKEN }}
timeout-minutes: 360
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "always() && always() && contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
enso-build-cli-ci-gen-job-upload-backend-linux:
name: Upload Backend (linux)
needs:
- enso-build-cli-ci-gen-draft-release-linux
runs-on:
- self-hosted
- Linux
- engine
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- run: ./run backend upload
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "always() && always() && contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
enso-build-cli-ci-gen-job-upload-backend-macos:
name: Upload Backend (macos)
needs:
- enso-build-cli-ci-gen-draft-release-linux
runs-on:
- macos-latest
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- run: ./run backend upload
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "always() && always() && contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
enso-build-cli-ci-gen-job-upload-backend-windows:
name: Upload Backend (windows)
needs:
- enso-build-cli-ci-gen-draft-release-linux
runs-on:
- self-hosted
- Windows
- engine
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- run: ./run backend upload
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "always() && always() && contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
enso-build-cli-ci-gen-publish-release-linux:
name: Publish release (linux)
needs:
- enso-build-cli-ci-gen-draft-release-linux
- enso-build-cli-ci-gen-job-deploy-gui-linux
- enso-build-cli-ci-gen-job-deploy-runtime-linux
- enso-build-cli-ci-gen-upload-ide-linux
- enso-build-cli-ci-gen-upload-ide-macos
- enso-build-cli-ci-gen-upload-ide-windows
runs-on:
- self-hosted
- Linux
- engine
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- run: ./run release publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "always() && always() && contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
AWS_ACCESS_KEY_ID: ${{ secrets.ARTEFACT_S3_ACCESS_KEY_ID }}
AWS_REGION: us-west-1
AWS_SECRET_ACCESS_KEY: ${{ secrets.ARTEFACT_S3_SECRET_ACCESS_KEY }}
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
enso-build-cli-ci-gen-upload-ide-linux:
name: Build IDE (linux)
needs:
- enso-build-cli-ci-gen-draft-release-linux
- enso-build-cli-ci-gen-job-build-wasm-linux
- enso-build-cli-ci-gen-job-upload-backend-linux
runs-on:
- self-hosted
- Linux
- engine
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- run: ./run ide upload --wasm-source current-ci-run --backend-source release --backend-release ${{env.ENSO_RELEASE_ID}}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "always() && always() && contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
enso-build-cli-ci-gen-upload-ide-macos:
name: Build IDE (macos)
needs:
- enso-build-cli-ci-gen-draft-release-linux
- enso-build-cli-ci-gen-job-build-wasm-linux
- enso-build-cli-ci-gen-job-upload-backend-macos
runs-on:
- macos-latest
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- run: ./run ide upload --wasm-source current-ci-run --backend-source release --backend-release ${{env.ENSO_RELEASE_ID}}
env:
APPLEID: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLEIDPASS: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
CSC_IDENTITY_AUTO_DISCOVERY: "true"
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CODE_SIGNING_CERT_PASSWORD }}
CSC_LINK: ${{ secrets.APPLE_CODE_SIGNING_CERT }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "always() && always() && contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
enso-build-cli-ci-gen-upload-ide-windows:
name: Build IDE (windows)
needs:
- enso-build-cli-ci-gen-draft-release-linux
- enso-build-cli-ci-gen-job-build-wasm-linux
- enso-build-cli-ci-gen-job-upload-backend-windows
runs-on:
- self-hosted
- Windows
- engine
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- run: ./run ide upload --wasm-source current-ci-run --backend-source release --backend-release ${{env.ENSO_RELEASE_ID}}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.MICROSOFT_CODE_SIGNING_CERT_PASSWORD }}
WIN_CSC_LINK: ${{ secrets.MICROSOFT_CODE_SIGNING_CERT }}
timeout-minutes: 360
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "always() && always() && contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
promote-nightly:
name: Promote nightly
uses: ./.github/workflows/promote.yml
with:
designator: nightly
secrets: inherit
env:
ENSO_BUILD_KIND: nightly
ENSO_BUILD_SKIP_VERSION_CHECK: "true"
RUST_BACKTRACE: full
concurrency: release

97
.github/workflows/promote.yml vendored Normal file
View File

@ -0,0 +1,97 @@
name: Generate a new version
on:
workflow_dispatch:
inputs:
designator:
description: What kind of release should be promoted.
required: true
type: choice
options:
- stable
- patch
- rc
- nightly
workflow_call:
inputs:
designator:
description: What kind of release should be promoted.
required: true
type: string
jobs:
enso-build-cli-ci-gen-promote-release-job-linux:
name: Promote release (linux)
runs-on:
- self-hosted
- Linux
- engine
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Setup conda (GH runners only)
uses: s-weigand/setup-conda@v1.0.5
with:
update-conda: false
conda-channels: anaconda, conda-forge
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v6
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- if: runner.os == 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (Windows)
run: '"c:\Program Files\Git\bin\bash.exe" -c "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"'
shell: cmd
- if: runner.os != 'Windows'
name: Workaround for https://github.com/actions/checkout/issues/590 (non-Windows)
run: "git checkout -f $(git -c user.name=x -c user.email=x@x commit-tree $(git hash-object -t tree /dev/null) < /dev/null) || :"
shell: bash
- name: Checking out the repository
uses: actions/checkout@v2
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: "contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- id: promote
run: ./run release promote ${{ inputs.designator }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "always() && always() && contains(github.event.pull_request.labels.*.name, 'CI: Clean build required')"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
outputs:
ENSO_EDITION: ${{ steps.promote.outputs.ENSO_EDITION }}
ENSO_RELEASE_MODE: ${{ steps.promote.outputs.ENSO_RELEASE_MODE }}
ENSO_VERSION: ${{ steps.promote.outputs.ENSO_VERSION }}
release:
name: Release
needs:
- enso-build-cli-ci-gen-promote-release-job-linux
uses: ./.github/workflows/release.yml
with:
version: ${{ needs.enso-build-cli-ci-gen-promote-release-job-linux.outputs.ENSO_VERSION }}
secrets: inherit
env:
ENSO_BUILD_SKIP_VERSION_CHECK: "true"

View File

@ -1,76 +0,0 @@
name: Publish Edition to S3
on:
release:
types: [published]
env:
# Please ensure that this is in sync with graalVersion in build.sbt
graalVersion: 22.3.0
# 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.5.2
# Please ensure that this is in sync with NIGHTLIES_TO_KEEP in nightly.yml
NIGHTLIES_TO_KEEP: 20
concurrency: "releases"
jobs:
update-editions-on-s3:
name: Update Editions on S3
runs-on: ubuntu-18.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v2
with:
path: repo
- 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: Prepare AWS Session
shell: bash
run: |
aws configure --profile s3-upload <<-EOF > /dev/null 2>&1
${{ secrets.ARTEFACT_S3_ACCESS_KEY_ID }}
${{ secrets.ARTEFACT_S3_SECRET_ACCESS_KEY }}
eu-central-1
text
EOF
- name: Update the Editions Bucket
shell: bash
working-directory: repo
run: sbt edition-uploader/run
- name: Teardown AWS Session
shell: bash
run: |
aws configure --profile s3-upload <<-EOF > /dev/null 2>&1
null
null
null
text
EOF

View File

@ -1,9 +1,20 @@
name: Release Candidate
name: Release
on:
workflow_dispatch: {}
workflow_dispatch:
inputs:
version:
description: What version number this release should get.
required: true
type: string
workflow_call:
inputs:
version:
description: What version number this release should get.
required: true
type: string
jobs:
enso-build-cli-ci-gen-draft-release-linux:
name: Create release draft
name: Create a release draft.
runs-on:
- self-hosted
- Linux
@ -248,8 +259,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
ENSO_RELEASE_ID: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID }}
ENSO_VERSION: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION }}
enso-build-cli-ci-gen-job-upload-backend-linux:
name: Upload Backend (linux)
needs:
@ -315,8 +326,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
ENSO_RELEASE_ID: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID }}
ENSO_VERSION: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION }}
enso-build-cli-ci-gen-job-upload-backend-macos:
name: Upload Backend (macos)
needs:
@ -380,8 +391,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
ENSO_RELEASE_ID: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID }}
ENSO_VERSION: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION }}
enso-build-cli-ci-gen-job-upload-backend-windows:
name: Upload Backend (windows)
needs:
@ -447,8 +458,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
ENSO_RELEASE_ID: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID }}
ENSO_VERSION: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION }}
enso-build-cli-ci-gen-publish-release-linux:
name: Publish release (linux)
needs:
@ -522,8 +533,8 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.ARTEFACT_S3_ACCESS_KEY_ID }}
AWS_REGION: us-west-1
AWS_SECRET_ACCESS_KEY: ${{ secrets.ARTEFACT_S3_SECRET_ACCESS_KEY }}
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
ENSO_RELEASE_ID: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID }}
ENSO_VERSION: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION }}
enso-build-cli-ci-gen-upload-ide-linux:
name: Build IDE (linux)
needs:
@ -591,8 +602,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
ENSO_RELEASE_ID: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID }}
ENSO_VERSION: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION }}
enso-build-cli-ci-gen-upload-ide-macos:
name: Build IDE (macos)
needs:
@ -663,8 +674,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
ENSO_RELEASE_ID: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID }}
ENSO_VERSION: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION }}
enso-build-cli-ci-gen-upload-ide-windows:
name: Build IDE (windows)
needs:
@ -734,10 +745,11 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 360
env:
ENSO_RELEASE_ID: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID}}
ENSO_VERSION: ${{needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION}}
ENSO_RELEASE_ID: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_RELEASE_ID }}
ENSO_VERSION: ${{ needs.enso-build-cli-ci-gen-draft-release-linux.outputs.ENSO_VERSION }}
env:
ENSO_BUILD_KIND: rc
ENSO_BUILD_SKIP_VERSION_CHECK: "true"
ENSO_EDITION: ${{ inputs.version }}
ENSO_VERSION: ${{ inputs.version }}
RUST_BACKTRACE: full
concurrency: release

75
Cargo.lock generated
View File

@ -1865,6 +1865,7 @@ dependencies = [
"futures 0.3.25",
"futures-util",
"glob",
"handlebars",
"heck",
"humantime 2.1.0",
"ide-ci",
@ -3566,6 +3567,20 @@ version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]]
name = "handlebars"
version = "4.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433e4ab33f1213cdc25b5fa45c76881240cfe79284cf2b395e8b9e312a30a2fd"
dependencies = [
"log 0.4.17",
"pest",
"pest_derive",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -3897,6 +3912,7 @@ dependencies = [
"lazy_static",
"log 0.4.17",
"mime 0.3.16",
"multimap",
"new_mime_guess",
"nix",
"octocrab",
@ -4641,6 +4657,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bba551d6d795f74a01767577ea8339560bf0a65354e0417b7e915ed608443d46"
[[package]]
name = "multimap"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
dependencies = [
"serde",
]
[[package]]
name = "multipart"
version = "0.18.0"
@ -5226,6 +5251,50 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pest"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8"
dependencies = [
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fd9bc6500181952d34bd0b2b0163a54d794227b498be0b7afa7698d0a7b18f"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2610d5ac5156217b4ff8e46ddcef7cdf44b273da2ac5bca2ecbfa86a330e7c4"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824749bf7e21dd66b36fbe26b3f45c713879cccd4a009a917ab8e045ca8246fe"
dependencies = [
"once_cell",
"pest",
"sha1 0.10.5",
]
[[package]]
name = "pin-project"
version = "1.0.12"
@ -7118,6 +7187,12 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "ucd-trie"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]]
name = "unicase"
version = "1.4.2"

View File

@ -9,7 +9,7 @@ use std::any::type_name;
/// An equivalent of standard's library `std::str::FromStr` trait, but with nice error messages.
pub trait FromString: Sized {
/// Parse a string into a value of this type. See: `std::str::FromStr::from_str`.
/// Parse a string into a value of this type. See: [`std::str::FromStr::from_str`].
fn from_str(s: &str) -> Result<Self>;
/// Parse a string into a value of this type and then convert it to `R`.

View File

@ -46,6 +46,16 @@ pub trait TryFutureExt: TryFuture {
self.map_err(|err| err.into().context(context)).boxed()
}
/// Convert the error type of this future to [`anyhow::Error`] and add the context.
fn with_context<F, C>(self, context: F) -> BoxFuture<'static, Result<Self::Ok>>
where
Self: Sized + Send + 'static,
std::result::Result<Self::Ok, Self::Error>: anyhow::Context<Self::Ok, Self::Error>,
F: FnOnce() -> C + Send + Sync + 'static,
C: Display + Send + Sync + 'static, {
self.into_future().map(|res| res.with_context(context)).boxed()
}
/// Convert the error type of this future to [`anyhow::Error`].
fn anyhow_err(self) -> MapErr<Self, fn(Self::Error) -> anyhow::Error>
where

View File

@ -22,9 +22,11 @@ pub trait PathExt: AsRef<Path> {
/// Appends a new extension to the file.
///
/// Does not try to replace previous extension, unlike `set_extension`.
/// Does not try to replace previous extension, unlike [`PathBuf::set_extension`].
/// Does nothing when given extension string is empty.
///
/// # Examples
///
/// ```
/// use enso_build_base::extensions::path::PathExt;
/// use std::path::PathBuf;
@ -141,7 +143,20 @@ pub trait PathExt: AsRef<Path> {
impl<T: AsRef<Path>> PathExt for T {}
/// A method that displays a value using `Display` trait.
/// A method that outputs a path to a formatter using [`Path::display`].
///
/// This is useful in combination with macros like `Derivative`, as demonstrated in the example
/// below.
///
/// # Example
/// ```ignore
/// #[derive(Derivative)]
/// #[derivative(Debug)]
/// pub struct Foo {
/// #[derivative(Debug(format_with = "display_fmt"))]
/// path: PathBuf,
/// }
/// ```
pub fn display_fmt(path: &Path, f: &mut Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&path.display(), f)
}

View File

@ -2,6 +2,9 @@
use crate::prelude::*;
use futures::future::Either;
use std::future::Ready;
/// Extension methods for [`Result`].
@ -11,9 +14,9 @@ pub trait ResultExt<T, E>: Sized {
fn map_async<'a, T2, F, Fut>(
self,
f: F,
) -> futures::future::Either<
) -> Either<
futures::future::Map<Fut, fn(T2) -> std::result::Result<T2, E>>,
std::future::Ready<std::result::Result<T2, E>>,
Ready<std::result::Result<T2, E>>,
>
where
F: FnOnce(T) -> Fut,
@ -24,7 +27,7 @@ pub trait ResultExt<T, E>: Sized {
fn and_then_async<'a, T2, E2, F, Fut>(
self,
f: F,
) -> futures::future::Either<Fut, std::future::Ready<std::result::Result<T2, E2>>>
) -> Either<Fut, Ready<std::result::Result<T2, E2>>>
where
F: FnOnce(T) -> Fut,
Fut: Future<Output = std::result::Result<T2, E2>> + Send + 'a,
@ -33,6 +36,13 @@ pub trait ResultExt<T, E>: Sized {
E2: Send + 'a;
/// Executes another future if this is an error. The error value is passed to a closure to
/// create this subsequent future.
fn or_else_async<F, Fut>(self, f: F) -> Either<Ready<Self>, futures::future::IntoFuture<Fut>>
where
F: FnOnce(E) -> Fut,
Fut: TryFuture<Ok = T, Error = E>;
/// Convert the error type to [`anyhow::Error`].
///
/// If there are additional context-specific information, use [`context`] instead.
@ -42,10 +52,7 @@ pub trait ResultExt<T, E>: Sized {
/// Convert the `[Result]<[Future]>` to `Future<Result>`.
fn flatten_fut(
self,
) -> futures::future::Either<
std::future::Ready<std::result::Result<T::Ok, T::Error>>,
futures::future::IntoFuture<T>,
>
) -> Either<Ready<std::result::Result<T::Ok, T::Error>>, futures::future::IntoFuture<T>>
where T: TryFuture<Error: From<E>>;
}
@ -53,9 +60,9 @@ impl<T, E> ResultExt<T, E> for std::result::Result<T, E> {
fn map_async<'a, T2, F, Fut>(
self,
f: F,
) -> futures::future::Either<
) -> Either<
futures::future::Map<Fut, fn(T2) -> std::result::Result<T2, E>>,
std::future::Ready<std::result::Result<T2, E>>,
Ready<std::result::Result<T2, E>>,
>
where
F: FnOnce(T) -> Fut,
@ -67,11 +74,24 @@ impl<T, E> ResultExt<T, E> for std::result::Result<T, E> {
}
}
fn or_else_async<'a, F, Fut>(
self,
f: F,
) -> Either<Ready<Self>, futures::future::IntoFuture<Fut>>
where
F: FnOnce(E) -> Fut,
Fut: TryFuture<Ok = T, Error = E>,
{
match self {
Ok(v) => ready(Ok(v)).left_future(),
Err(e) => f(e).into_future().right_future(),
}
}
fn and_then_async<'a, T2, E2, F, Fut>(
self,
f: F,
) -> futures::future::Either<Fut, std::future::Ready<std::result::Result<T2, E2>>>
) -> Either<Fut, Ready<std::result::Result<T2, E2>>>
where
F: FnOnce(T) -> Fut,
Fut: Future<Output = std::result::Result<T2, E2>> + Send + 'a,
@ -92,10 +112,7 @@ impl<T, E> ResultExt<T, E> for std::result::Result<T, E> {
fn flatten_fut(
self,
) -> futures::future::Either<
std::future::Ready<std::result::Result<T::Ok, T::Error>>,
futures::future::IntoFuture<T>,
>
) -> Either<Ready<std::result::Result<T::Ok, T::Error>>, futures::future::IntoFuture<T>>
where T: TryFuture<Error: From<E>> {
match self {
Ok(fut) => fut.into_future().right_future(),

View File

@ -29,7 +29,7 @@ futures = "0.3.17"
futures-util = "0.3.17"
glob = "0.3.0"
#graphql_client = "0.10.0"
#handlebars = "4.2.1"
handlebars = "4.3.5"
heck = "0.4.0"
humantime = "2.1.0"
enso-build-base = { path = "../base" }

View File

@ -8,6 +8,7 @@
changelog.yml:
gui.yml:
nightly.yml:
promote.yml:
release.yml:
scala-new.yml:
app/:

View File

@ -0,0 +1,71 @@
{{!-- This is a template of the markdown text that is used as a release body.
Please see [this](src/release.rs) for the code that uses this template and
available placeholders.
The template itself is written in [Handlebars](https://handlebarsjs.com/).
--}}
# Download
## Enso IDE
Enso IDE is the main product of the Enso project. The packages are stand-alone,
they contain both GUI and the backend.
Download links:
- [Linux]({{download_prefix}}/enso-linux-{{version}}.AppImage) (AppImage);
- [macOS]({{download_prefix}}/enso-mac-{{version}}.dmg) (DMG);
- [Windows]({{download_prefix}}/enso-win-{{version}}.exe) (Installer
Executable).
This is the recommended download for most users.
## Enso Engine
If you are interested in using Enso Engine command line tools only, download the
Enso Engine bundle.
Download links:
- [Linux]({{download_prefix}}/enso-bundle-{{version}}-linux-amd64.tar.gz);
- [macOS]({{download_prefix}}/enso-bundle-{{version}}-macos-amd64.tar.gz);
- [Windows]({{download_prefix}}/enso-bundle-{{version}}-windows-amd64.zip).
These are archives containing the
[Enso portable distribution](https://enso.org/docs/developer/enso/distribution/distribution.html#portable-enso-distribution-layout).
User is responsible for setting up the environment variables and adding the
`bin` directory to the `PATH`.
Note that these distributions do not allow you to use the Enso IDE.
It is recommended only for advanced users, who want to just try the compiler
CLI.
# Changelog
{{changelog}}
# Anonymous Data Collection
Please note that this release collects anonymous usage data which will be used
to improve Enso and prepare it for a stable release. We will switch to opt-in
data collection in stable version releases. The usage data will not contain your
code (expressions above nodes), however, reported errors may contain brief
snippets of out of context code that specifically leads to the error, like "the
method 'foo' does not exist on Number". The following data will be collected:
- Session length.
- Graph editing events (node creation, deletion, position change, connect,
disconnect, collapse, edit start, edit end). This will not include any
information about node expressions used.
- Navigation events (camera movement, scope change).
- Visualization events (visualization open, close, switch). This will not
include any information about the displayed data nor the rendered
visualization itself.
- Project management events (project open, close, rename).
- Errors (IDE crashes, WASM panics, Project Manager errors, Language Server
errors, Compiler errors).
- Performance statistics (minimum, maximum, average GUI refresh rate).

View File

@ -4,7 +4,6 @@ use crate::paths::TargetTriple;
use derivative::Derivative;
use ide_ci::github;
use ide_ci::programs::git;
use octocrab::models::repos::Release;
use octocrab::models::ReleaseId;
@ -29,12 +28,16 @@ pub struct BuildContext {
}
impl BuildContext {
/// Get the current commit hash.
///
/// If there is GITHUB_SHA environment variable, it is used. Otherwise, the current commit hash
/// is determined using `git` command.
pub fn commit(&self) -> BoxFuture<'static, Result<String>> {
let root = self.repo_root.to_path_buf();
let git = self.git();
async move {
match ide_ci::actions::env::GITHUB_SHA.get() {
Ok(commit) => Ok(commit),
Err(_e) => git::Context::new(root).await?.head_hash().await,
Err(_e) => git.await?.head_hash().await,
}
}
.boxed()
@ -57,11 +60,9 @@ impl BuildContext {
tag => repository.find_release_by_text(tag).await?,
}
};
Ok(release)
Result::Ok(release)
}
.map_err(move |e: anyhow::Error| {
e.context(format!("Failed to resolve release designator `{designator_cp}`."))
})
.with_context(move || format!("Failed to resolve release designator `{}`", designator_cp))
.boxed()
}

View File

@ -64,7 +64,7 @@ impl Bundle for ProjectManager {
}
}
// #[context("Placing a GraalVM package under {}", target_directory.as_ref().display())]
#[context("Placing a GraalVM package under {}", target_directory.as_ref().display())]
pub async fn place_graal_under(target_directory: impl AsRef<Path>) -> Result {
let graal_path = {
let java_home = JAVA_HOME.get()?;

View File

@ -96,7 +96,7 @@ impl RunContext {
let prepare_simple_library_server = {
if self.config.test_scala {
let simple_server_path = &self.paths.repo_root.tools.simple_library_server;
ide_ci::programs::git::Context::new(simple_server_path)
ide_ci::programs::git::new(simple_server_path)
.await?
.cmd()?
.clean()

View File

@ -112,34 +112,53 @@ pub fn retrieve_github_access_token() -> Result<String> {
fn get_token_from_file() -> Result<String> {
let path =
dirs::home_dir().context("Failed to locate home directory.")?.join("GITHUB_TOKEN");
debug!("Looking for GitHub token in the file {}", path.display());
let content = ide_ci::fs::read_to_string(path)?;
Ok(content.trim().into())
}
ide_ci::env::expect_var("GITHUB_TOKEN")
.inspect(|_| debug!("Will use GITHUB_TOKEN environment variable."))
.inspect_err(|e| debug!("Failed to retrieve GitHub authentication from environment: {e}"))
.or_else(|_| get_token_from_file())
}
#[context("Failed to setup GitHub API client.")]
pub async fn setup_octocrab() -> Result<Octocrab> {
let mut builder = octocrab::OctocrabBuilder::new();
if let Ok(access_token) = retrieve_github_access_token() {
builder = builder.personal_token(access_token);
let octocrab = builder.build()?;
match octocrab.ratelimit().get().await {
Ok(rate) => info!(
"GitHub API rate limit: {}/{}.",
rate.resources.core.used, rate.resources.core.limit
),
Err(e) => bail!(
"Failed to get rate limit info: {e}. GitHub Personal Access Token might be invalid."
),
}
Ok(octocrab)
let builder = octocrab::OctocrabBuilder::new();
let octocrab = if let Ok(access_token) = retrieve_github_access_token() {
let octocrab = builder.personal_token(access_token).build()?;
let username = octocrab
.current()
.user()
.await
.inspect_err(|e| warn!("Failed to retrieve GitHub username: {e}"))
.map_or_else(|_| "N/A".to_string(), |user| user.login);
info!("Using GitHub API with personal access token. Authenticated as {username}.",);
octocrab
} else {
builder.build().anyhow_err()
info!("No GitHub Personal Access Token found. Will use anonymous API access.");
warn!(
"Anonymous GitHub API access is rate-limited. If you are experiencing issues, please \
set the GITHUB_TOKEN environment variable."
);
warn!(
"Additionally some APIs may not be available to anonymous users. This primarily \
pertains the release-related APIs."
);
builder.build()?
};
match octocrab.ratelimit().get().await {
Ok(rate) => info!(
"GitHub API rate limit: {}/{}.",
rate.resources.core.used, rate.resources.core.limit
),
Err(e) => bail!(
"Failed to get rate limit info: {e}. GitHub Personal Access Token might be invalid."
),
}
Ok(octocrab)
}
#[cfg(test)]

View File

@ -16,6 +16,7 @@ use ide_ci::actions::artifacts;
use ide_ci::cache;
use ide_ci::cache::Cache;
use ide_ci::ok_ready_boxed;
use ide_ci::programs::git;
use octocrab::models::repos::Asset;
@ -80,8 +81,8 @@ impl<T> PlainArtifact<T> {
pub struct Context {
/// GitHub API client.
///
/// If authorized, it will count API rate limits against our identity and allow operations like
/// managing releases or downloading CI run artifacts.
/// If authenticated, it will count API rate limits against our identity and allow operations
/// like managing releases or downloading CI run artifacts.
#[derivative(Debug = "ignore")]
pub octocrab: Octocrab,
@ -99,6 +100,14 @@ pub struct Context {
pub repo_root: crate::paths::generated::RepoRoot,
}
impl Context {
/// Get a `git` program handle for the repository.
pub fn git(&self) -> impl Future<Output = Result<git::Context>> + 'static {
let root = self.repo_root.to_path_buf();
git::new(root)
}
}
/// Build targets, like GUI or Project Manager.
///
/// Built target generates artifacts that can be stored as a release asset or CI run artifacts.

View File

@ -1,5 +1,8 @@
//! Support code for managing Enso releases.
use crate::prelude::*;
use crate::changelog::Changelog;
use crate::context::BuildContext;
use crate::paths::generated;
use crate::paths::TargetTriple;
@ -10,34 +13,119 @@ use crate::project::Gui;
use crate::project::IsTarget;
use crate::source::ExternalSource;
use crate::source::FetchTargetJob;
use crate::version;
use crate::version::promote::Designation;
use crate::version::Versions;
use ide_ci::github;
use ide_ci::github::release::ReleaseHandle;
use ide_ci::io::web::handle_error_response;
use ide_ci::programs::Docker;
use ide_ci::programs::SevenZip;
use octocrab::models::repos::Release;
use octocrab::params::repos::Reference;
use reqwest::Response;
use serde_json::json;
use tempfile::tempdir;
pub fn release_from_env(context: &BuildContext) -> Result<ReleaseHandle> {
let release_id = crate::env::ENSO_RELEASE_ID.get()?;
Ok(ReleaseHandle::new(&context.octocrab, context.remote_repo.clone(), release_id))
/// Get the prefix of URL of the release's asset in GitHub.
///
/// By joining it with the asset name, we can get the URL of the asset.
///
/// ```
/// # use enso_build::prelude::*;
/// # use enso_build::release::download_asset_prefix;
/// # use ide_ci::github::RepoRef;
/// let repo = RepoRef::new("enso-org", "enso");
/// let version = Version::from_str("2020.1.1").unwrap();
/// let prefix = download_asset_prefix(&repo, &version);
/// assert_eq!(prefix, "https://github.com/enso-org/enso/releases/download/2020.1.1");
/// ```
pub fn download_asset_prefix(repo: &impl IsRepo, version: &Version) -> String {
format!("https://github.com/{repo}/releases/download/{version}",)
}
pub async fn draft_a_new_release(context: &BuildContext) -> Result<Release> {
let versions = &context.triple.versions;
let commit = ide_ci::actions::env::GITHUB_SHA.get()?;
/// Generate placeholders for the release notes.
pub fn release_body_placeholders(
context: &BuildContext,
) -> Result<BTreeMap<&'static str, serde_json::Value>> {
let mut ret = BTreeMap::new();
ret.insert(
"is_stable",
(!version::Kind::deduce(&context.triple.versions.version)?.is_prerelease()).into(),
);
ret.insert("version", context.triple.versions.version.to_string().into());
ret.insert("edition", context.triple.versions.edition_name().into());
ret.insert("repo", serde_json::to_value(&context.remote_repo)?);
ret.insert(
"download_prefix",
format!(
"https://github.com/{}/releases/download/{}",
context.remote_repo, context.triple.versions.version
)
.into(),
);
// Generate the release notes.
let changelog_contents = ide_ci::fs::read_to_string(&context.repo_root.changelog_md)?;
let latest_changelog_body =
crate::changelog::Changelog(&changelog_contents).top_release_notes()?;
let latest_changelog_body = Changelog(&changelog_contents).top_release_notes()?;
ret.insert("changelog", latest_changelog_body.contents.into());
Ok(ret)
}
/// Generate the release notes.
///
/// They are generated from the template file `release-body.md` by replacing the placeholders.
pub async fn generate_release_body(context: &BuildContext) -> Result<String> {
let available_placeholders = release_body_placeholders(context)?;
let handlebars = handlebars::Handlebars::new();
let template = include_str!("../release-body.md");
let body = handlebars.render_template(template, &available_placeholders)?;
Ok(body)
}
/// Create a new release draft on GitHub.
///
/// As it is a draft, it will not be visible to the public until it is published.
#[context("Failed to draft a new release {} from the commit {}.", context.triple.versions.tag(),
commit)]
pub async fn draft_a_new_release(context: &BuildContext, commit: &str) -> Result<Release> {
let versions = &context.triple.versions;
let tag = versions.tag();
// We don't require being run from a git repository context, but if we do happen to be, then
// the local HEAD is required to be the same as the commit we read from environment.
if let Ok(git) = context.git().await {
let head_hash = git.head_hash().await?;
ensure!(
head_hash == commit,
"Local HEAD {} is not the same as the commit {} we were ordered to build.",
head_hash,
commit
);
}
// Make sure that this version has not been released yet.
let repo = &context.remote_repo_handle();
if let Ok(colliding_release) = repo.find_release_by_tag(&tag).await {
ensure!(colliding_release.draft, "Release {} has already been published.", tag);
warn!(
"Release {tag} has already been drafted: {}. Creating a new one.",
colliding_release.html_url
);
}
// Check if the remote repository contains a conflicting tag.
let colliding_remote_tag = repo.get_ref(&Reference::Tag(tag.clone())).await;
if let Ok(colliding_remote_tag) = colliding_remote_tag {
bail!("Tag {} already exists on remote as {:?}.", tag, colliding_remote_tag.object);
}
let is_prerelease = version::Kind::deduce(&versions.version)?.is_prerelease();
debug!("Preparing release {} for commit {}", versions.version, commit);
let release = context
.remote_repo_handle()
.repos()
@ -45,8 +133,8 @@ pub async fn draft_a_new_release(context: &BuildContext) -> Result<Release> {
.create(&versions.tag())
.target_commitish(&commit)
.name(&versions.pretty_name())
.body(&latest_changelog_body.contents)
.prerelease(true)
.body(&generate_release_body(context).await?)
.prerelease(is_prerelease)
.draft(true)
.send()
.await?;
@ -55,6 +143,9 @@ pub async fn draft_a_new_release(context: &BuildContext) -> Result<Release> {
Ok(release)
}
/// Publish a previously drafted release and update edition file in the cloud.
///
/// Requires the release ID to be set in the environment variable `ENSO_RELEASE_ID`.
pub async fn publish_release(context: &BuildContext) -> Result {
let remote_repo = context.remote_repo_handle();
let BuildContext { inner: project::Context { .. }, triple, .. } = context;
@ -210,6 +301,7 @@ pub async fn put_files_gzipping(
Ok(())
}
/// Deploy the built GUI (frontend) to the cloud.
#[context("Failed to notify the cloud about GUI upload in version {}.", version)]
pub async fn notify_cloud_about_gui(version: &Version) -> Result<Response> {
let body = json!({
@ -217,7 +309,7 @@ pub async fn notify_cloud_about_gui(version: &Version) -> Result<Response> {
"versionType": "Ide"
});
let response = reqwest::Client::new()
.post("https://nngmxi3zr4.execute-api.eu-west-1.amazonaws.com/versions")
.post("https://7aqkn3tnbc.execute-api.eu-west-1.amazonaws.com/versions")
.header("x-enso-organization-id", "org-2BqGX0q2yCdONdmx3Om1MVZzmv3")
.header("Content-Type", "application/json")
.json(&body)
@ -227,6 +319,28 @@ pub async fn notify_cloud_about_gui(version: &Version) -> Result<Response> {
handle_error_response(response).await
}
/// Generate a new version number of a requested kind.
pub async fn resolve_version_designation(
context: &BuildContext,
designation: Designation,
) -> Result<Version> {
let git = context.git().await?;
let version_set = version::promote::Releases::from_remote(&git).await?;
version_set.generate_version(designation)
}
/// Generate a new version and make it visible to CI.
///
/// Sets `ENSO_VERSION`, `ENSO_EDITION` and `ENSO_RELEASE_MODE` environment variables.
#[instrument]
pub async fn promote_release(context: &BuildContext, version_designation: Designation) -> Result {
let version = resolve_version_designation(context, version_designation).await?;
let versions = Versions::new(version);
versions.publish().await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -13,7 +13,7 @@ pub mod cloud;
/// repository.
///
/// Current heuristic is: contains Cargo workspace root and SBT build configuration file.
#[instrument(fields(path = %path.as_ref().display()), ret)]
#[instrument(level = "trace", fields(path = %path.as_ref().display()), ret)]
pub fn looks_like_enso_repository_root(path: impl AsRef<Path>) -> bool {
(move || -> Result<bool> {
let cargo_toml = path.as_ref().join("Cargo.toml");

View File

@ -1,6 +1,5 @@
use crate::prelude::*;
use ide_ci::github;
use ide_ci::github::RepoRef;
@ -13,10 +12,11 @@ pub const BUILD_IMAGE_WORKFLOW: &str = "build-image.yaml";
/// https://github.com/enso-org/cloud-v2/blob/main/.github/workflows/build-image.yaml#L4
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct BuildImageInput<T> {
runtime_version: T,
pub runtime_version: T,
}
impl<T> BuildImageInput<T> {
/// Create a new model of "build-image" workflow input.
pub fn new(runtime_version: T) -> Self {
Self { runtime_version }
}
@ -28,19 +28,7 @@ impl<T> BuildImageInput<T> {
#[instrument(fields(%version), skip(octocrab))]
pub async fn build_image_workflow_dispatch_input(octocrab: &Octocrab, version: &Version) -> Result {
let repo = CLOUD_REPO.handle(octocrab);
// We want to call our workflow on the default branch.
let default_branch = repo.get().await?.default_branch.with_context(|| {
format!(
"Failed to get the default branch of the {} repository. Missing field: `default_branch`.",
CLOUD_REPO
)
})?;
debug!("Will invoke on ref: '{}'", default_branch);
let input = BuildImageInput::new(version);
info!("Dispatching the cloud workflow to build the image.");
github::workflow::dispatch(&repo, BUILD_IMAGE_WORKFLOW, default_branch, &input).await
repo.dispatch_workflow(BUILD_IMAGE_WORKFLOW, &BuildImageInput::new(version.to_string())).await
}
#[cfg(test)]

View File

@ -1,4 +1,8 @@
//! Code that deals with the version of Enso.
//!
//! Enso uses [Semantic Versioning 2.0.0](https://semver.org/). By the convention the major version
//! number is fixed to the year of the release. The minor version number is incremented for each
//! (backwards incompatible) release.
use crate::prelude::*;
@ -10,18 +14,27 @@ use ide_ci::env::new::TypedVariable;
use ide_ci::github;
use octocrab::models::repos::Release;
use semver::Prerelease;
use std::collections::BTreeSet;
use strum::EnumIter;
use strum::EnumString;
use strum::IntoEnumIterator;
use tracing::instrument;
// ==============
// === Export ===
// ==============
pub mod nightly;
pub mod promote;
// Variable that stores Enso Engine version.
// They are also used by the SBT part of the build.
define_env_var! {
/// The version of Enso (shared by GUI and Engine).
ENSO_VERSION, Version;
/// Edition name for the build.
///
/// By convention, this is the same as the version.
@ -31,18 +44,14 @@ define_env_var! {
ENSO_RELEASE_MODE, bool;
}
pub const LOCAL_BUILD_PREFIX: &str = "dev";
pub const NIGHTLY_BUILD_PREFIX: &str = "nightly";
pub const RC_BUILD_PREFIX: &str = "rc";
/// Check if the given GitHub release matches the provided kind.
pub fn is_release_of_kind(release: &Release, kind: Kind) -> bool {
match kind {
Kind::Dev => release.tag_name.contains(LOCAL_BUILD_PREFIX),
Kind::Nightly => release.tag_name.contains(NIGHTLY_BUILD_PREFIX),
Kind::Rc => release.tag_name.contains(RC_BUILD_PREFIX),
Kind::Stable => !release.prerelease,
}
matches!(release.tag_name.parse2(), Ok(version) if kind.matches(&version))
}
/// List all releases in the GitHub repository that are of a given kind.
@ -54,6 +63,7 @@ pub async fn releases_of_kind(
}
/// Get the latest nightly release in the GitHub repository.
#[context("Failed to get the latest nightly release in {repo}.")]
pub async fn latest_nightly_release(repo: &github::repo::Handle<impl IsRepo>) -> Result<Release> {
// TODO: this assumes that releases are returned in date order, to be confirmed
// (but having to download all the pages to see which is latest wouldn't be nice)
@ -110,74 +120,6 @@ impl Versions {
Prerelease::new(LOCAL_BUILD_PREFIX).anyhow_err()
}
pub async fn nightly_prerelease(
repo: &github::repo::Handle<impl IsRepo>,
) -> Result<Prerelease> {
let date = chrono::Utc::now();
let date = date.format("%F").to_string();
let todays_pre_text = format!("{}.{}", NIGHTLY_BUILD_PREFIX, date);
let generate_ith = |index: u32| -> Result<Prerelease> {
let pre = if index == 0 {
Prerelease::from_str(&todays_pre_text)?
} else {
Prerelease::from_str(&format!("{}.{}", todays_pre_text, index))?
};
Ok(pre)
};
let relevant_nightly_versions = releases_of_kind(repo, Kind::Nightly)
.await?
.filter_map(|release| {
if release.tag_name.contains(&todays_pre_text) {
let version = Version::parse(&release.tag_name).ok()?;
Some(version.pre)
} else {
None
}
})
.collect::<BTreeSet<_>>();
// Generate subsequent tonight nightly subreleases, until a free one is found.
// Should happen rarely.
for index in 0.. {
let pre = generate_ith(index)?;
if !relevant_nightly_versions.contains(&pre) {
return Ok(pre);
}
}
unreachable!("After infinite loop.")
}
/// Generate prerelease string for the "release candidate" release.
///
/// We list all the RC releases in the repository, and increment the number of the latest one.
pub async fn rc_prerelease(
version: &Version,
repo: &github::repo::Handle<impl IsRepo>,
) -> Result<Prerelease> {
let relevant_rc_versions = releases_of_kind(repo, Kind::Rc)
.await?
.filter_map(|release| {
let release_version = Version::parse(&release.tag_name).ok()?;
let version_matches = release_version.major == version.major
&& release_version.minor == version.minor
&& release_version.patch == version.patch;
version_matches.then_some(release_version.pre)
})
.collect::<BTreeSet<_>>();
// Generate subsequent RC sub-releases, until a free one is found.
// Should happen rarely.
for index in 0.. {
let pre = Prerelease::from_str(&format!("{}.{}", RC_BUILD_PREFIX, index))?;
if !relevant_rc_versions.contains(&pre) {
return Ok(pre);
}
}
unreachable!("After infinite loop.")
}
/// Get a git tag that should be applied to a commit released as this version.
pub fn tag(&self) -> String {
self.version.to_string()
@ -219,12 +161,13 @@ pub fn base_version(changelog_path: impl AsRef<Path>) -> Result<Version> {
.iterate_headers()
.map(|h| Version::find_in_text(h.text));
let year = current_year();
let version = match headers.next() {
None => generate_initial_version(),
None => generate_initial_version(year),
Some(Ok(top_version)) => top_version,
Some(Err(_top_non_version_thingy)) => match headers.next() {
Some(Ok(version)) => suggest_next_version(&version),
None => generate_initial_version(),
Some(Ok(version)) => suggest_next_version(&version, year),
None => generate_initial_version(year),
Some(Err(_)) => bail!("Two leading release headers have no version number in them."),
},
};
@ -235,21 +178,66 @@ pub fn current_year() -> u64 {
chrono::Utc::now().year() as u64
}
pub fn generate_initial_version() -> Version {
Version::new(current_year(), 1, 1)
pub fn generate_initial_version(current_year: u64) -> Version {
Version::new(current_year, 1, 1)
}
pub fn suggest_next_version(previous: &Version) -> Version {
let year = current_year();
if previous.major == year {
Version::new(year, previous.minor + 1, 1)
pub fn suggest_next_version(previous: &Version, current_year: u64) -> Version {
if previous.major == current_year {
Version::new(current_year, previous.minor + 1, 1)
} else {
generate_initial_version()
generate_initial_version(current_year)
}
}
/// Generate a next RC version for the given version.
///
/// ```
/// use enso_build::prelude::*;
/// use enso_build::version::increment_rc_version;
/// let rc_version = Version::from_str("0.2.3-rc.1").unwrap();
/// assert_eq!(increment_rc_version(&rc_version).unwrap().to_string(), "0.2.3-rc.2");
/// ```
pub fn increment_rc_version(version: &Version) -> Result<Version> {
ensure!(Kind::Rc.matches(version), "Version is not an RC version: {}.", version);
match version.pre.split('.').collect_vec().as_slice() {
[RC_BUILD_PREFIX, index] => {
let index = index.parse2::<u32>().context("Parsing RC index.")?;
let pre = generate_rc_prerelease(index + 1)?;
Ok(Version { pre, ..version.clone() })
}
_ => bail!("RC version has an unexpected prerelease: {}.", version),
}
}
/// Compare version core, i.e. the part that is not a prerelease/build.
///
/// This is used to determine if a version is a prerelease of another version.
///
/// # Example
/// ```
/// use enso_build::prelude::*;
/// use enso_build::version::same_core_version;
///
/// let version = Version::from_str("1.2.3").unwrap();
/// assert!(same_core_version(&version, &version));
/// let version_beta = Version::from_str("1.2.3-beta").unwrap();
/// assert!(same_core_version(&version, &version_beta));
/// let version_build = Version::from_str("1.2.3+build").unwrap();
/// assert!(same_core_version(&version, &version_build));
/// let other_version = Version::from_str("1.2.4").unwrap();
/// assert!(!same_core_version(&version, &other_version));
/// ```
pub fn same_core_version(a: &Version, b: &Version) -> bool {
a.major == b.major && a.minor == b.minor && a.patch == b.patch
}
pub fn generate_rc_prerelease(index: u32) -> Result<Prerelease> {
Prerelease::from_str(&format!("{}.{}", RC_BUILD_PREFIX, index))
}
#[instrument(ret)]
pub fn versions_from_env(expected_build_kind: Option<Kind>) -> Result<Option<Versions>> {
pub fn versions_from_env() -> Result<Option<Versions>> {
if let Ok(version) = ENSO_VERSION.get() {
// The currently adopted version scheme uses same string for version and edition name,
// so we enforce it here. There are no fundamental reasons for this requirement.
@ -261,15 +249,6 @@ pub fn versions_from_env(expected_build_kind: Option<Kind>) -> Result<Option<Ver
ENSO_EDITION.name
);
}
if let Some(expected_build_kind) = expected_build_kind {
let found_build_kind = Kind::deduce(&version)?;
ensure!(
found_build_kind == expected_build_kind,
"Build kind mismatch. Found: {}, expected: {}.",
found_build_kind,
expected_build_kind
)
}
let versions = Versions::new(version);
Ok(Some(versions))
} else {
@ -278,51 +257,19 @@ pub fn versions_from_env(expected_build_kind: Option<Kind>) -> Result<Option<Ver
}
#[instrument(skip_all, ret)]
pub async fn deduce_or_generate(
repo: Result<&github::repo::Handle<impl IsRepo>>,
kind: Kind,
root_path: impl AsRef<Path>,
) -> Result<Versions> {
pub async fn deduce_or_generate<P, F>(release_provider: P) -> Result<Versions>
where
P: FnOnce() -> F,
F: Future<Output = Result<Vec<Version>>>, {
debug!("Deciding on version to target.");
if let Some(versions) = versions_from_env(Some(kind))? {
if let Some(versions) = versions_from_env()? {
Ok(versions)
} else {
let changelog_path = crate::paths::root_to_changelog(&root_path);
let base_version = base_version(&changelog_path)?;
let version = Version {
pre: match kind {
Kind::Dev => Versions::local_prerelease()?,
Kind::Nightly => Versions::nightly_prerelease(repo?).await?,
Kind::Rc => Versions::rc_prerelease(&base_version, repo?).await?,
Kind::Stable => todo!(), //Versions::stable(repo?).await?,
},
..base_version
};
Ok(Versions::new(version))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_nightly_test() {
let is_nightly = |text: &str| Kind::Nightly.matches(&Version::parse(text).unwrap());
assert!(is_nightly("2022.1.1-nightly.2022.1.1"));
assert!(is_nightly("2022.1.1-nightly"));
assert!(is_nightly("2022.1.1-nightly.2022.1.1"));
assert!(is_nightly("2022.1.1-nightly.2022.1.1"));
let version = Version::parse("2022.1.1-nightly.2022-06-06.3").unwrap();
assert!(Kind::deduce(&version).contains(&Kind::Nightly));
}
#[test]
#[ignore]
fn iii() -> Result {
dbg!(base_version(r"H:\nbo\enso\app\gui\changelog.md")?);
Ok(())
let releases = release_provider().await?;
let releases = promote::Releases::new_now(releases)?;
let next_stable = releases.generate_version(promote::Designation::Stable)?;
let dev_version = Version { pre: Prerelease::from_str(LOCAL_BUILD_PREFIX)?, ..next_stable };
Ok(Versions::new(dev_version))
}
}
@ -347,22 +294,79 @@ pub enum Kind {
}
impl Kind {
pub fn prerelease_prefix(self) -> &'static str {
/// Get the first piece of prerelease identifier that is used to designate this kind of version.
///
/// Returns `None` if this kind of version requires empty prerelease.
pub fn prerelease_prefix(self) -> Option<&'static str> {
match self {
Kind::Dev => LOCAL_BUILD_PREFIX,
Kind::Nightly => NIGHTLY_BUILD_PREFIX,
Kind::Rc => RC_BUILD_PREFIX,
Kind::Stable => "",
Kind::Dev => Some(LOCAL_BUILD_PREFIX),
Kind::Nightly => Some(NIGHTLY_BUILD_PREFIX),
Kind::Rc => Some(RC_BUILD_PREFIX),
Kind::Stable => None,
}
}
/// Check if the given version number matches this kind.
///
/// ```
/// use enso_build::version::Kind;
/// use semver::Version;
///
/// assert!(Kind::Nightly.matches(&Version::parse("2022.1.1-nightly.2022.1.1").unwrap()));
/// assert!(Kind::Nightly.matches(&Version::parse("2022.1.1-nightly").unwrap()));
/// assert!(Kind::Rc.matches(&Version::parse("2022.1.1-rc.1").unwrap()));
/// assert!(!Kind::Nightly.matches(&Version::parse("2022.1.1-rc.1").unwrap()));
/// assert!(!Kind::Stable.matches(&Version::parse("2022.1.1-rc.1").unwrap()));
/// assert!(!Kind::Stable.matches(&Version::parse("2022.1.1-nightly").unwrap()));
/// assert!(Kind::Stable.matches(&Version::parse("2022.1.1").unwrap()));
/// ```
pub fn matches(self, version: &Version) -> bool {
version.pre.as_str().starts_with(self.prerelease_prefix())
if let Some(prefix) = self.prerelease_prefix() {
version.pre.starts_with(prefix)
} else {
version.pre.is_empty()
}
}
/// Deduce the kind of the version.
///
/// ```
/// use enso_build::prelude::*;
/// use enso_build::version::Kind;
///
/// fn main() -> Result {
/// assert_eq!(Kind::deduce(&Version::parse("2022.1.1-dev")?)?, Kind::Dev);
/// assert_eq!(Kind::deduce(&Version::parse("2022.1.1-nightly.2022.1.1")?)?, Kind::Nightly);
/// assert_eq!(Kind::deduce(&Version::parse("2022.1.1-rc.1")?)?, Kind::Rc);
/// assert_eq!(Kind::deduce(&Version::parse("2022.1.1")?)?, Kind::Stable);
/// Ok(())
/// }
/// ```
pub fn deduce(version: &Version) -> Result<Self> {
Kind::iter()
.find(|kind| kind.matches(version))
.context(format!("Failed to deduce build kind for version {version}"))
.with_context(|| format!("Failed to deduce build kind for version {version}"))
}
/// Check if this is one of the prerelease kinds.
pub fn is_prerelease(self) -> bool {
self != Kind::Stable
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_nightly_test() {
let is_nightly = |text: &str| Kind::Nightly.matches(&Version::parse(text).unwrap());
assert!(is_nightly("2022.1.1-nightly.2022.1.1"));
assert!(is_nightly("2022.1.1-nightly"));
assert!(is_nightly("2022.1.1-nightly.2022.1.1"));
assert!(is_nightly("2022.1.1-nightly.2022.1.1"));
let version = Version::parse("2022.1.1-nightly.2022-06-06.3").unwrap();
assert!(Kind::deduce(&version).contains(&Kind::Nightly));
}
}

View File

@ -0,0 +1,184 @@
use crate::prelude::*;
use crate::version;
use chrono::Datelike;
use semver::Prerelease;
use semver::Version;
/// Parsed nightly build [prerelease](https://semver.org/#spec-item-9) piece.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct NightlyPrerelease {
/// The date of the nightly build.
pub date: chrono::NaiveDate,
/// The number of the nightly build.
///
/// The first nightly build of the day has None, subsequent builds have Some(1), Some(2), etc.
pub index: Option<u64>,
}
impl NightlyPrerelease {
#[allow(missing_docs)]
pub fn new(date: chrono::NaiveDate, index: Option<u64>) -> Self {
Self { date, index }
}
/// Generate a next nightly build version for the given date.
///
/// ```
/// use enso_build::version::nightly::NightlyPrerelease;
/// let date = chrono::NaiveDate::from_ymd_opt(2020, 1, 1).unwrap();
/// let first = NightlyPrerelease::new(date, None);
/// let second = first.next();
/// assert_eq!(second, NightlyPrerelease::new(date, Some(1)));
/// let third = second.next();
/// assert_eq!(third, NightlyPrerelease::new(date, Some(2)));
/// ```
pub fn next(mut self) -> Self {
self.index = Some(self.index.unwrap_or(0) + 1);
self
}
}
impl TryFrom<&Prerelease> for NightlyPrerelease {
type Error = anyhow::Error;
#[context("Failed to parse nightly version prerelease: `{prerelease}`.")]
fn try_from(prerelease: &Prerelease) -> std::result::Result<Self, Self::Error> {
let prerelease = prerelease.as_str();
let identifiers = prerelease.split('.').collect_vec();
ensure!(
identifiers.first().contains(&&version::NIGHTLY_BUILD_PREFIX),
"Not a nightly build."
);
ensure!(identifiers.len() == 4 || identifiers.len() == 5, "Wrong number of identifiers.");
let year = identifiers.get(1).context("Missing year")?.parse2().context("Invalid year")?;
let month =
identifiers.get(2).context("Missing month")?.parse2().context("Invalid month")?;
let day = identifiers.get(3).context("Missing day")?.parse2().context("Invalid day")?;
let index =
identifiers.get(4).map(|index| index.parse2()).transpose().context("Invalid index")?;
let date = chrono::NaiveDate::from_ymd_opt(year, month, day)
.with_context(|| format!("Invalid date: {}-{}-{}", year, month, day))?;
Ok(Self::new(date, index))
}
}
impl Display for NightlyPrerelease {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let Self { date, index } = self;
write!(
f,
"{}.{}.{}.{}",
version::NIGHTLY_BUILD_PREFIX,
date.year(),
date.month(),
date.day()
)?;
if let Some(index) = index {
write!(f, ".{}", index)?;
}
Ok(())
}
}
impl TryInto<Prerelease> for NightlyPrerelease {
type Error = anyhow::Error;
fn try_into(self) -> std::result::Result<Prerelease, Self::Error> {
let as_string = self.to_string();
Prerelease::from_str(&as_string)
}
}
impl TryFrom<&Version> for NightlyPrerelease {
type Error = anyhow::Error;
#[context("Failed to parse nightly version: `{value}`.")]
fn try_from(value: &Version) -> std::result::Result<Self, Self::Error> {
Self::try_from(&value.pre)
}
}
/// Nightly build version.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Nightly {
full_version: Version,
prerelease: NightlyPrerelease,
}
impl Nightly {
pub fn new(full_version: Version) -> anyhow::Result<Self> {
let prerelease = NightlyPrerelease::try_from(&full_version)?;
Ok(Self { full_version, prerelease })
}
pub fn next(&self) -> Self {
let prerelease = self.prerelease.next();
let full_version =
Version { pre: prerelease.try_into().unwrap(), ..self.full_version.clone() };
Self { full_version, prerelease }
}
/// Get as [`Version`].
pub fn version(&self) -> &Version {
&self.full_version
}
/// Get the parser prerelease piece of the version.
pub fn prerelease(&self) -> &NightlyPrerelease {
&self.prerelease
}
}
impl TryFrom<&Version> for Nightly {
type Error = anyhow::Error;
#[context("Failed to parse nightly version: `{value}`.")]
fn try_from(value: &Version) -> std::result::Result<Self, Self::Error> {
let prerelease = NightlyPrerelease::try_from(value)?;
Ok(Self { full_version: value.clone(), prerelease })
}
}
impl From<Nightly> for Version {
fn from(value: Nightly) -> Self {
value.full_version
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parsing_nightly_prerelease() -> Result {
let nightly = Version::from_str("2020.1.1-nightly.2020.12.31")?;
assert_eq!(NightlyPrerelease::try_from(&nightly)?, NightlyPrerelease {
date: chrono::NaiveDate::from_ymd_opt(2020, 12, 31).unwrap(),
index: None,
});
let nightly_with_index = Version::from_str("2020.1.1-nightly.2020.12.31.1")?;
assert_eq!(NightlyPrerelease::try_from(&nightly_with_index)?, NightlyPrerelease {
date: chrono::NaiveDate::from_ymd_opt(2020, 12, 31).unwrap(),
index: Some(1),
});
let nightly_with_too_many_identifiers =
Version::from_str("2020.1.1-nightly.2020.12.31.1.1")?;
assert!(NightlyPrerelease::try_from(&nightly_with_too_many_identifiers).is_err());
let nightly_with_missing_year = Version::from_str("2020.1.1-nightly.12.31")?;
assert!(NightlyPrerelease::try_from(&nightly_with_missing_year).is_err());
let non_nightly = Version::from_str("2020.1.1")?;
assert!(NightlyPrerelease::try_from(&non_nightly).is_err());
let rc = Version::from_str("2020.1.1-rc.1")?;
assert!(NightlyPrerelease::try_from(&rc).is_err());
Ok(())
}
}

View File

@ -0,0 +1,198 @@
use crate::prelude::*;
use crate::version;
use crate::version::nightly::Nightly;
use crate::version::nightly::NightlyPrerelease;
use chrono::Datelike;
use ide_ci::programs::git;
use ide_ci::programs::git::Ref;
/// Describes what kind of version should be generated.
#[derive(
clap::ArgEnum,
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
strum::EnumString,
strum::EnumIter,
strum::AsRefStr
)]
#[strum(serialize_all = "lowercase")]
pub enum Designation {
/// Create a new stable release.
Stable,
/// Create a new patch release.
Patch,
/// Create a new RC release.
Rc,
/// Create a new nightly release.
Nightly,
}
pub async fn releases_on_remote(git: &git::Context) -> Result<Vec<Version>> {
let remote_tags = git.list_remote_tags().await?;
Ok(remote_tags
.into_iter()
.filter_map(|entry| match entry.r#ref {
Ref::Tag { name, .. } => Version::from_str(&name).ok(),
_ => None,
})
.collect())
}
/// List of all releases on the remote.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Releases {
/// A list of all released versions.
pub versions: BTreeSet<Version>,
/// Current date.
///
/// It is used to determine major version in some cases, we keep it as parameter to keep the
/// code testable. Also, defines a day for nightly releases.
pub date: chrono::NaiveDate,
}
impl Releases {
/// Create, while using the current date.
pub fn new_now(versions: impl IntoIterator<Item = Version>) -> Result<Self> {
let date = chrono::Utc::now().naive_utc().date();
Self::new(versions, date)
}
/// Create with a given date.
pub fn new(
versions: impl IntoIterator<Item = Version>,
date: chrono::NaiveDate,
) -> Result<Self> {
let versions: BTreeSet<_> = versions.into_iter().collect();
for version in &versions {
ensure!(
version.major <= (date.year() as u64),
"Found version from the future: {}.",
version
);
}
Ok(Self { versions, date })
}
/// Generate, based on all releases on the remote.
pub async fn from_remote(git: &git::Context) -> Result<Self> {
let date = chrono::Utc::now();
let versions = releases_on_remote(git).await?;
Self::new(versions, date.date_naive())
}
/// Iterates over releases of a given [`version::Kind`].
pub fn of_kind(&self, kind: version::Kind) -> impl Iterator<Item = &Version> {
self.versions.iter().filter(move |version| kind.matches(version))
}
/// Get the latest release of a given [`version::Kind`].
pub fn latest_of_kind(&self, kind: version::Kind) -> Option<&Version> {
self.of_kind(kind).max()
}
/// Generate a version for next release of a given [`Designation`].
pub fn generate_version(&self, designation: Designation) -> Result<Version> {
let latest_stable = self.latest_of_kind(version::Kind::Stable);
let next_stable = if let Some(latest_stable) = latest_stable {
version::suggest_next_version(latest_stable, self.date.year() as u64)
} else {
version::generate_initial_version(self.date.year() as u64)
};
let ret = match designation {
Designation::Stable => next_stable,
Designation::Patch => {
let latest_stable = latest_stable.with_context(|| {
"No stable releases found. There must be some stable release before creating \
a patch release."
})?;
Version::new(latest_stable.major, latest_stable.minor, latest_stable.patch + 1)
}
Designation::Rc => {
let last_relevant_rc = self
.of_kind(version::Kind::Rc)
.filter(|version| version::same_core_version(version, &next_stable))
.max();
if let Some(last_relevant_rc) = last_relevant_rc {
version::increment_rc_version(last_relevant_rc)?
} else {
Version { pre: version::generate_rc_prerelease(1)?, ..next_stable }
}
}
Designation::Nightly => {
let last_relevant_nightly = self
.of_kind(version::Kind::Nightly)
.filter(|v| v.same_core(&next_stable))
.filter_map(|v| Nightly::try_from(v).ok())
.filter(|v| v.prerelease().date == self.date)
.max();
if let Some(last_relevant_nightly) = last_relevant_nightly {
last_relevant_nightly.next().into()
} else {
let prerelease = NightlyPrerelease::new(self.date, None);
Version { pre: prerelease.try_into()?, ..next_stable }
}
}
};
Ok(ret)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn expect_next_release(
date: chrono::NaiveDate,
releases_so_far: impl IntoIterator<Item: AsRef<str>>,
designation: Designation,
expected: &str,
) {
let releases_so_far =
releases_so_far.into_iter().map(|version| Version::from_str(version.as_ref()).unwrap());
let releases = Releases::new(releases_so_far, date).unwrap();
let next_release = releases.generate_version(designation).unwrap();
assert_eq!(next_release.to_string(), expected);
}
#[test]
fn test_version_generation() -> Result {
use Designation::*;
let previously_released_versions = ["2021.1.1", "2021.1.2"];
let date = chrono::NaiveDate::from_ymd_opt(2021, 2, 22).context("Invalid date")?;
let next_date = chrono::NaiveDate::from_ymd_opt(2021, 2, 23).context("Invalid date")?;
expect_next_release(date, previously_released_versions, Stable, "2021.2.1");
expect_next_release(date, previously_released_versions, Patch, "2021.1.3");
expect_next_release(date, previously_released_versions, Rc, "2021.2.1-rc.1");
expect_next_release(
date,
previously_released_versions,
Nightly,
"2021.2.1-nightly.2021.2.22",
);
let previously_released_versions = ["2021.1.1", "2021.1.2", "2021.2.1-nightly.2021.2.22"];
expect_next_release(
date,
previously_released_versions,
Nightly,
"2021.2.1-nightly.2021.2.22.1",
);
expect_next_release(
next_date,
previously_released_versions,
Nightly,
"2021.2.1-nightly.2021.2.23",
);
Ok(())
}
}

View File

@ -40,6 +40,7 @@ itertools = "0.10.1"
lazy_static = "1.4.0"
log = "0.4.14"
mime = "0.3.16"
multimap = "0.8.3"
new_mime_guess = "4.0.0"
nix = { workspace = true }
octocrab = { git = "https://github.com/enso-org/octocrab", default-features = false, features = [

View File

@ -22,10 +22,12 @@ pub fn is_in_env() -> bool {
///
/// See: <https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#setting-an-output-parameter>
pub async fn set_output(name: &str, value: &(impl ToString + ?Sized)) -> Result {
let value = value.to_string();
if is_in_env() {
let value = value.to_string();
debug!("Setting GitHub Actions step output {name} to {value}.");
env_file::GITHUB_OUTPUT.append_key_value(name, &value).await?;
} else {
debug!("Not setting GitHub Actions step output {name} to {value} because we are not in GitHub Actions environment.");
}
Ok(())
}

View File

@ -178,23 +178,29 @@ impl Workflow {
key
}
pub fn add<J: JobArchetype>(&mut self, os: OS) -> String {
self.add_customized::<J>(os, |_| {})
pub fn add(&mut self, os: OS, job: impl JobArchetype) -> String {
self.add_customized(os, job, |_| {})
}
pub fn add_customized<J: JobArchetype>(&mut self, os: OS, f: impl FnOnce(&mut Job)) -> String {
let (key, mut job) = J::entry(os);
pub fn add_customized(
&mut self,
os: OS,
job: impl JobArchetype,
f: impl FnOnce(&mut Job),
) -> String {
let (key, mut job) = job.entry(os);
f(&mut job);
self.jobs.insert(key.clone(), job);
key
}
pub fn add_dependent<J: JobArchetype>(
pub fn add_dependent(
&mut self,
os: OS,
job: impl JobArchetype,
needed: impl IntoIterator<Item: AsRef<str>>,
) -> String {
let (key, mut job) = J::entry(os);
let (key, mut job) = job.entry(os);
for needed in needed {
self.expose_outputs(needed.as_ref(), &mut job);
}
@ -308,7 +314,7 @@ pub enum WorkflowDispatchInputType {
Choice {
#[serde(skip_serializing_if = "Option::is_none")]
default: Option<String>,
choices: Vec<String>, // should be non-empty
options: Vec<String>, // should be non-empty
},
Boolean {
#[serde(skip_serializing_if = "Option::is_none")]
@ -355,14 +361,32 @@ impl WorkflowDispatchInput {
pub fn new_string(
description: impl Into<String>,
required: bool,
default: impl Into<String>,
default: Option<impl Into<String>>,
) -> Self {
Self {
r#type: WorkflowDispatchInputType::String { default: Some(default.into()) },
r#type: WorkflowDispatchInputType::String { default: default.map(Into::into) },
..Self::new(description, required)
}
}
pub fn new_choice(
description: impl Into<String>,
required: bool,
options: impl IntoIterator<Item: Into<String>>,
default: Option<impl Into<String>>,
) -> Result<Self> {
let options = options.into_iter().map(Into::into).collect_vec();
ensure!(!options.is_empty(), "The options for a choice input must not be empty.");
let default = default.map(Into::into);
if let Some(default) = &default {
ensure!(options.contains(default), "The default value must be one of the options.");
}
Ok(Self {
r#type: WorkflowDispatchInputType::Choice { default, options },
..Self::new(description, required)
})
}
pub fn new_boolean(description: impl Into<String>, required: bool, default: bool) -> Self {
Self {
r#type: WorkflowDispatchInputType::Boolean { default: Some(default) },
@ -394,8 +418,115 @@ impl WorkflowDispatch {
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum WorkflowCallInputType {
String {
#[serde(skip_serializing_if = "Option::is_none")]
default: Option<String>,
},
Boolean {
#[serde(skip_serializing_if = "Option::is_none")]
default: Option<bool>,
},
Number {
#[serde(skip_serializing_if = "Option::is_none")]
default: Option<f64>,
},
}
impl Default for WorkflowCallInputType {
fn default() -> Self {
Self::String { default: None }
}
}
impl TryFrom<WorkflowDispatchInputType> for WorkflowCallInputType {
type Error = anyhow::Error;
fn try_from(value: WorkflowDispatchInputType) -> Result<Self> {
Ok(match value {
WorkflowDispatchInputType::String { default } => Self::String { default },
WorkflowDispatchInputType::Boolean { default } => Self::Boolean { default },
WorkflowDispatchInputType::Choice { default, .. } => Self::String { default },
WorkflowDispatchInputType::Environment { .. } =>
bail!("Environment is not supported for workflow call inputs!"),
})
}
}
/// Input to the workflow_call trigger.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct WorkflowCallInput {
/// A string description of the input parameter.
pub description: String,
/// A boolean to indicate whether the action requires the input parameter.
pub required: bool,
/// A string representing the type of the input.
#[serde(flatten)]
pub r#type: WorkflowCallInputType,
}
impl TryFrom<WorkflowDispatchInput> for WorkflowCallInput {
type Error = anyhow::Error;
fn try_from(value: WorkflowDispatchInput) -> Result<Self> {
Ok(Self {
description: value.description,
required: value.required,
r#type: value.r#type.try_into()?,
})
}
}
/// A workflow call output.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct WorkflowCallOutput {
/// A string description of the output parameter.
pub description: String,
/// Expression to defining the output parameter.
pub value: String,
}
/// A workflow call secret parameter.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WorkflowCallSecret {
/// A string description of the secret parameter.
pub description: String,
/// A boolean specifying whether the secret must be supplied.
pub required: bool,
}
/// A workflow call trigger.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct WorkflowCall {
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub inputs: BTreeMap<String, WorkflowCallInput>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub outputs: BTreeMap<String, WorkflowCallOutput>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub secrets: BTreeMap<String, WorkflowCallSecret>,
}
impl TryFrom<WorkflowDispatch> for WorkflowCall {
type Error = anyhow::Error;
fn try_from(value: WorkflowDispatch) -> Result<Self> {
Ok(Self {
inputs: value
.inputs
.into_iter()
.map(|(k, v)| Result::Ok((k, v.try_into()?)))
.try_collect()?,
..Default::default()
})
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Event {
#[serde(skip_serializing_if = "Option::is_none")]
pub push: Option<Push>,
@ -405,6 +536,8 @@ pub struct Event {
pub schedule: Vec<Schedule>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow_dispatch: Option<WorkflowDispatch>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow_call: Option<WorkflowCall>,
}
impl Event {
@ -433,6 +566,13 @@ impl Event {
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum JobSecrets {
Inherit,
Map(BTreeMap<String, String>),
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Job {
@ -441,7 +581,9 @@ pub struct Job {
pub needs: BTreeSet<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub r#if: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub runs_on: Vec<RunnerLabel>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub steps: Vec<Step>,
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency: Option<Concurrency>,
@ -453,6 +595,12 @@ pub struct Job {
pub strategy: Option<Strategy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout_minutes: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uses: Option<String>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub with: BTreeMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secrets: Option<JobSecrets>,
}
impl Job {
@ -464,7 +612,12 @@ impl Job {
let step = step_id.as_ref();
let output = output_name.into();
let value = format!("${{{{ steps.{step}.outputs.{output} }}}}");
self.outputs.insert(output, value);
if let Some(old_value) = self.outputs.insert(output.clone(), value) {
warn!(
"Overwriting output `{}` of job `{}` with value `{}`",
output, self.name, old_value
);
}
}
pub fn env(&mut self, name: impl Into<String>, value: impl Into<String>) {
@ -478,7 +631,7 @@ impl Job {
pub fn use_job_outputs(&mut self, job_id: impl Into<String>, job: &Job) {
let job_id = job_id.into();
for output_name in job.outputs.keys() {
let reference = format!("${{{{needs.{}.outputs.{}}}}}", job_id, output_name);
let reference = wrap_expression(format!("needs.{job_id}.outputs.{output_name}"));
self.env.insert(output_name.into(), reference);
}
self.needs(job_id);
@ -487,6 +640,15 @@ impl Job {
pub fn needs(&mut self, job_id: impl Into<String>) {
self.needs.insert(job_id.into());
}
pub fn with(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.with.insert(name.into(), value.into());
}
pub fn with_with(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.with(name, value);
self
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
@ -768,27 +930,27 @@ pub fn checkout_repo_step() -> impl IntoIterator<Item = Step> {
}
pub trait JobArchetype {
fn id_key_base() -> String {
fn id_key_base(&self) -> String {
std::any::type_name::<Self>().to_kebab_case()
}
fn key(os: OS) -> String {
format!("{}-{}", Self::id_key_base(), os)
fn key(&self, os: OS) -> String {
format!("{}-{}", self.id_key_base(), os)
}
fn job(os: OS) -> Job;
fn job(&self, os: OS) -> Job;
fn entry(os: OS) -> (String, Job) {
(Self::key(os), Self::job(os))
fn entry(&self, os: OS) -> (String, Job) {
(self.key(os), self.job(os))
}
// [Step ID] => [variable names]
fn outputs() -> BTreeMap<String, Vec<String>> {
fn outputs(&self) -> BTreeMap<String, Vec<String>> {
default()
}
fn expose_outputs(job: &mut Job) {
for (step_id, outputs) in Self::outputs() {
fn expose_outputs(&self, job: &mut Job) {
for (step_id, outputs) in self.outputs() {
for output in outputs {
job.expose_output(&step_id, output);
}

View File

@ -223,6 +223,12 @@ pub mod new {
}
}
impl<Value, Borrowed: ?Sized> Display for SimpleVariable<Value, Borrowed> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
#[derive(Clone, Copy, Debug, Display, Ord, PartialOrd, Eq, PartialEq)]
pub struct PathLike(pub &'static str);

View File

@ -1,16 +1,54 @@
use crate::prelude::*;
use semver::Prerelease;
pub trait VersionExt {
fn triple(&self) -> (u64, u64, u64);
fn same_triple(&self, other: &Self) -> bool {
self.triple() == other.triple()
fn core(&self) -> (u64, u64, u64);
fn same_core(&self, other: &Self) -> bool {
self.core() == other.core()
}
/// Get the identifiers (i.e. the dot-separated parts after the hyphen) of the prerelease.
fn identifiers(&self) -> Vec<&str>;
}
impl VersionExt for Version {
fn triple(&self) -> (u64, u64, u64) {
fn core(&self) -> (u64, u64, u64) {
(self.major, self.minor, self.patch)
}
fn identifiers(&self) -> Vec<&str> {
self.pre.identifiers()
}
}
pub trait PrereleaseExt {
/// Get the identifiers (i.e. the dot-separated parts) of the prerelease.
fn identifiers(&self) -> Vec<&str>;
}
impl PrereleaseExt for Prerelease {
fn identifiers(&self) -> Vec<&str> {
if self.is_empty() {
default()
} else {
self.split('.').collect()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_identifiers() -> Result {
let version = Version::parse("1.2.3-alpha.1")?;
assert_eq!(version.identifiers(), vec!["alpha", "1"]);
let version = Version::parse("1.2.3")?;
assert_eq!(version.identifiers(), Vec::<&str>::new());
Ok(())
}
}

View File

@ -16,14 +16,15 @@ pub async fn open(path: impl AsRef<Path>) -> Result<File> {
File::open(&path).await.anyhow_err()
}
// #[context("Failed to open path for reading: {}", path.as_ref().display())]
pub fn open_stream(path: impl AsRef<Path>) -> BoxFuture<'static, Result<ReaderStream<File>>> {
let error_message = format!("Failed to open path for reading: {}", path.as_ref().display());
let path = path.as_ref().to_owned();
let file = open(path);
async move {
let file = file.await?;
Ok(ReaderStream::new(file))
Result::Ok(ReaderStream::new(file))
}
.context(error_message)
.boxed()
}

View File

@ -58,7 +58,6 @@ pub trait IsReleaseExt: IsRelease + Sync {
/// Upload a new asset to the release from a given file.
///
/// The filename will be used to name the asset and deduce MIME content type.
// #[context("Failed to upload an asset from the file under {}", path.as_ref().display())]
#[instrument(skip_all, fields(source = %path.as_ref().display()))]
async fn upload_asset_file(&self, path: impl AsRef<Path> + Send) -> Result<Asset> {
let error_msg =

View File

@ -8,6 +8,7 @@ use crate::github::MAX_PER_PAGE;
use headers::HeaderMap;
use headers::HeaderValue;
use octocrab::models::repos::Asset;
use octocrab::models::repos::Ref;
use octocrab::models::repos::Release;
use octocrab::models::workflows::WorkflowListArtifact;
use octocrab::models::ArtifactId;
@ -15,6 +16,7 @@ use octocrab::models::AssetId;
use octocrab::models::ReleaseId;
use octocrab::models::RunId;
use octocrab::params::actions::ArchiveFormat;
use octocrab::params::repos::Reference;
use reqwest::Response;
@ -194,14 +196,31 @@ impl<R: IsRepo> Handle<R> {
.context(format!("Failed to find release by id `{release_id}` in `{self}`."))
}
#[tracing::instrument(fields(%self), skip(predicate), err)]
pub async fn find_release_if(&self, predicate: impl Fn(&Release) -> bool) -> Result<Release> {
let releases = self.all_releases().await?;
let release = releases.into_iter().find(predicate);
release.context("Failed to find a release that satisfies the predicate.")
}
#[tracing::instrument(fields(%self, %text), err)]
pub async fn find_release_by_text(&self, text: &str) -> anyhow::Result<Release> {
self.all_releases()
.await?
.into_iter()
.find(|release| release.tag_name.contains(text))
self.find_release_if(|release| release.tag_name.contains(text))
.await
.inspect(|release| info!("Found release at: {} (id={}).", release.html_url, release.id))
.context(format!("No release with tag matching `{text}` in {self}."))
.with_context(|| format!("No release with tag matching `{text}` in {self}."))
}
#[tracing::instrument(fields(%self, %text), err)]
pub async fn find_release_by_tag(&self, text: &str) -> anyhow::Result<Release> {
self.find_release_if(|release| release.tag_name == text)
.await
.inspect(|release| info!("Found release at: {} (id={}).", release.html_url, release.id))
.with_context(|| format!("No release with tag equal to `{text}` in {self}."))
}
pub async fn get_ref(&self, r#ref: &Reference) -> Result<Ref> {
self.repos().get_ref(r#ref).await.context(format!("Failed to get ref `{ref}` in {self}."))
}
#[tracing::instrument(fields(%self, %run_id, %name), err, ret)]
@ -302,4 +321,24 @@ impl<R: IsRepo> Handle<R> {
.await
.with_context(|| format!("Failed to get the infomation for the {self} repository."))
}
/// Get the name of the default branch.
pub async fn default_branch(&self) -> Result<String> {
self.get().await?.default_branch.with_context(|| {
format!(
"Failed to get the default branch of the {} repository. Missing field: `default_branch`.",
self
)
})
}
/// Dispatches the workflow using the head of the default branch.
pub async fn dispatch_workflow(
&self,
workflow_id: impl AsRef<str> + Send + Sync + 'static,
inputs: &impl Serialize,
) -> Result {
let default_branch = self.default_branch().await?;
crate::github::workflow::dispatch(self, workflow_id, default_branch, inputs).await
}
}

View File

@ -115,6 +115,8 @@ pub mod prelude {
pub use crate::env::new::TypedVariable as _;
pub use crate::extensions::clap::ArgExt as _;
pub use crate::extensions::command::CommandExt as _;
pub use crate::extensions::version::PrereleaseExt as _;
pub use crate::extensions::version::VersionExt as _;
pub use crate::github::release::IsReleaseExt as _;
pub use crate::program::command::provider::CommandProviderExt as _;
pub use crate::program::version::IsVersion as _;
@ -141,6 +143,8 @@ pub const USER_AGENT: &str = "enso-build";
pub const UNREGISTERED_PORTS: Range<u16> = 49152..65535;
pub const RECORD_SEPARATOR: &str = "\u{1E}";
/// Looks up a free port in the IANA private or dynamic port range.
pub fn get_free_port() -> Result<u16> {
let port_range = UNREGISTERED_PORTS;

View File

@ -16,6 +16,7 @@ use tracing_subscriber::Registry;
pub fn is_our_module_path(path: impl AsRef<str>) -> bool {
// true
["ide_ci::", "enso"].into_iter().any(|prefix| path.as_ref().starts_with(prefix))
}

View File

@ -1,6 +1,14 @@
//! Wrappers over git commands.
//!
//! As a reasonable starting points, please refer to:
//! * [`new`] to obtain a context for an existing repository.
//! * [`init`] to initialize a new repository.
use crate::prelude::*;
use crate::new_command_type;
use crate::programs::git::pretty_format::refs_from_decoration;
use crate::RECORD_SEPARATOR;
// ==============
@ -8,11 +16,23 @@ use crate::new_command_type;
// ==============
pub mod clean;
pub mod pretty_format;
pub mod r#ref;
pub use clean::Clean;
pub use r#ref::Ref;
/// The default name of the remote repository.
///
/// If use case arises, we can make it configurable in the future.
const DEFAULT_REMOTE: &str = "origin";
/// Git version control system.
///
/// See: <https://git-scm.com/>
#[derive(Clone, Copy, Debug)]
pub struct Git;
@ -25,11 +45,14 @@ impl Program for Git {
impl Git {
/// Create a new, empty git repository in the given directory.
pub fn init(&self, path: impl AsRef<Path>) -> Result<GitCommand> {
let mut cmd = self.cmd()?;
cmd.arg(Command::Init);
cmd.current_dir(path);
Ok(cmd)
#[context("Failed to initialize git repository in {}", path.as_ref().display())]
pub async fn init(&self, path: impl AsRef<Path>) -> Result<Context> {
crate::fs::tokio::create_dir_if_missing(path.as_ref()).await?;
self.cmd()?.arg(Command::Init).current_dir(path.as_ref()).run_ok().await?;
Ok(Context {
working_dir: path.as_ref().to_path_buf(),
repository_root: path.as_ref().to_path_buf(),
})
}
}
@ -45,11 +68,15 @@ pub struct Context {
repository_root: PathBuf,
/// Directory in which commands will be invoked.
/// It might not be the repository root and it makes difference for many commands.
/// If it is different from the repository root, it must be in its subdirectory.
working_dir: PathBuf,
}
impl Context {
/// Initialize a new command invoking git.
///
/// This should be used as a starting point for all git commands.
/// The command comes with current working directory set, please do not override it.
pub fn cmd(&self) -> Result<GitCommand> {
Ok(Git.cmd()?.with_current_dir(&self.working_dir))
}
@ -68,7 +95,7 @@ impl Context {
}
}
/// Create a `Git` invocation context within a given directory.
/// Create a `git` invocation context within a given directory.
///
/// The `working_dir` is the directory in which git commands will be invoked. It is expected to
/// be a part of some git repository.
@ -84,18 +111,26 @@ impl Context {
Ok(Context { repository_root: repo_root, working_dir: temp_git.working_dir })
}
/// Create a git context for the current working directory.
pub async fn new_current() -> Result<Self> {
Context::new(crate::env::current_dir()?).await
}
/// Get the hash of the current [HEAD commit](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefHEADaHEAD).
#[context("Failed to get a HEAD hash.")]
pub async fn head_hash(&self) -> Result<String> {
self.cmd()?.args(["rev-parse", "--verify", "HEAD"]).output_ok().await?.single_line_stdout()
self.cmd()?
.arg(Command::RevParse)
.args(["--verify", "HEAD"])
.output_ok()
.await?
.single_line_stdout()
}
/// Fetch a branch from a remote repository.
#[context("Failed to fetch branch {} from remote {}.", branch, remote)]
pub async fn fetch_branch(&self, remote: &str, branch: &str) -> Result {
self.cmd()?.args(["fetch", remote, branch]).run_ok().await
self.cmd()?.arg(Command::Fetch).args([remote, branch]).run_ok().await
}
/// List of files that are different than the compared commit.
@ -104,7 +139,8 @@ impl Context {
let root = self.repository_root.as_path();
Ok(self
.cmd()?
.args(["diff", "--name-only", compare_against.as_ref()])
.arg(Command::Diff)
.args(["--name-only", compare_against.as_ref()])
.output_ok()
.await?
.into_stdout_string()?
@ -113,16 +149,94 @@ impl Context {
.collect_vec())
}
/// Get the repository root directory.
pub async fn repository_root(&self) -> Result<PathBuf> {
let output = self
.cmd()?
.args(["rev-parse", "--show-toplevel"])
.arg(Command::RevParse)
.args(["--show-toplevel"])
.output_ok()
.await?
.single_line_stdout()?;
let path = PathBuf::from(output).normalize();
Ok(path)
}
/// Fetch the tags from the remote repository and prune the local tags that are no longer on the
/// remote.
pub async fn sync_tags(&self) -> Result {
trace!("Fetching the remotes and pruning tags.");
self.cmd()?.arg(Command::Fetch).args(["--prune", "--prune-tags", "--tags"]).run_ok().await
}
/// List the tags on the remote repository.
///
/// Note that tag objects will appear twice: once for the tag object itself, and once in the
/// dereferenced form (e.g. `refs/tags/enso-0.2.0^{}`), for the commit object that the tag
/// points to.
pub async fn list_remote_tags(&self) -> Result<Vec<RemoteLsEntry>> {
let output = self
.cmd()?
.arg(Command::LsRemote)
.args(["--tags", DEFAULT_REMOTE])
.output_ok()
.await?
.into_stdout_string()?;
output.lines().map(RemoteLsEntry::from_str).try_collect()
}
/// Get the commit history of the current branch.
///
/// Fails if there are no commits in the repository.
pub async fn log(&self) -> Result<Vec<LogEntry>> {
use pretty_format::Placeholder::Hash;
use pretty_format::Placeholder::RefNames;
let fields = [Hash, RefNames];
let history = self.log_fields(&fields).await?;
history
.into_iter()
.map(|fields| {
let mut fields = fields.into_iter();
let hash = fields.next().context("Missing hash.")?;
let refs = fields.next().context("Missing refs.")?;
let refs = refs_from_decoration(&refs);
let refs = refs.into_iter().map(|s| s.parse2()).try_collect()?;
Ok(LogEntry { hash, refs })
})
.try_collect()
}
/// Get the commit history of the current branch with the given fields.
pub async fn log_fields(
&self,
fields: &[pretty_format::Placeholder],
) -> Result<Vec<Vec<String>>> {
let field_count = fields.len();
let format = fields.iter().map(ToString::to_string).join(RECORD_SEPARATOR);
let output = self
.cmd()?
.arg(Command::Log)
.args(["--decorate=full", &format!("--pretty=format:{format}")])
.output_ok()
.await?
.into_stdout_string()?;
output
.lines()
.map(|line| {
let fields = line.split(RECORD_SEPARATOR).map(ToString::to_string).collect_vec();
ensure!(fields.len() == field_count, "Wrong number of fields in log entry.");
Ok(fields)
})
.try_collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct LogEntry {
pub hash: String,
pub refs: Vec<Ref>,
}
@ -140,7 +254,8 @@ impl GitCommand {
}
}
#[derive(Clone, Copy, Debug)]
/// A top-level command for git.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Command {
/// Remove untracked files from the working tree.
Clean,
@ -148,15 +263,75 @@ pub enum Command {
Diff,
/// Create an empty Git repository or reinitialize an existing one.
Init,
/// Download objects and refs from another repository.
Fetch,
/// Show commit logs.
Log,
/// List references in a remote repository.
LsRemote,
/// Pick out and massage parameters.
RevParse,
}
impl AsRef<OsStr> for Command {
fn as_ref(&self) -> &OsStr {
match self {
Command::Clean => OsStr::new("clean"),
Command::Diff => OsStr::new("diff"),
Command::Init => OsStr::new("init"),
}
OsStr::new(match self {
Command::Clean => "clean",
Command::Diff => "diff",
Command::Init => "init",
Command::Fetch => "fetch",
Command::Log => "log",
Command::LsRemote => "ls-remote",
Command::RevParse => "rev-parse",
})
}
}
/// Create a new git wrapper for the repository owning the given path.
///
/// Path is not required to be a repository root, but it must be inside the repository.
///
/// # Examples
/// ```
/// use enso_build_base::prelude::*;
/// use ide_ci::programs::git;
/// use ide_ci::programs::Git;
/// #[tokio::main]
/// async fn main() -> Result {
/// let temp = tempfile::tempdir()?;
/// // There is no repository yet, cannot create a context.
/// assert!(git::new(temp.path()).await.is_err());
///
/// // Initialize a new repository in the temp path.
/// Git.init(temp.path()).await?;
/// let git = git::new(temp.path()).await?;
/// Ok(())
/// }
/// ```
pub async fn new(path: impl Into<PathBuf>) -> Result<Context> {
Context::new(path).await
}
/// Reference available in a remote repository along with the associated commit ID.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct RemoteLsEntry {
/// Commit ID (hash).
pub hash: String,
/// Reference name.
pub r#ref: Ref,
}
/// Construct from a line of output of `git ls-remote`.
impl std::str::FromStr for RemoteLsEntry {
type Err = anyhow::Error;
#[context("Failed to parse remote ls entry from string: {}", line)]
fn from_str(line: &str) -> std::result::Result<Self, Self::Err> {
let mut parts = line.split_whitespace();
let hash = parts.next().context("Missing hash")?.to_string();
let r#ref = parts.next().context("Missing reference")?.parse2()?;
ensure!(parts.next().is_none(), "Unexpected trailing extra parts.");
Ok(Self { hash, r#ref })
}
}
@ -164,6 +339,34 @@ impl AsRef<OsStr> for Command {
mod tests {
use super::*;
#[tokio::test]
async fn github_wrapping_non_directory() -> Result {
let temp = tempfile::tempdir()?;
let git = new(temp.path()).await;
assert!(git.is_err());
Ok(())
}
#[tokio::test]
#[ignore]
async fn list_remote_tags_test() -> Result {
let git = Context::new_current().await?;
let tags = git.list_remote_tags().await?;
let map: multimap::MultiMap<_, _> =
tags.into_iter().map(|tag| (tag.hash, tag.r#ref)).collect();
dbg!(&map);
Ok(())
}
#[tokio::test]
#[ignore]
async fn log_test() -> Result {
let git = Context::new_current().await?;
dbg!(git.log().await)?;
Ok(())
}
#[tokio::test]
#[ignore]
async fn repo_root() -> Result {

View File

@ -125,7 +125,7 @@ mod tests {
setup_logging()?;
let dir = PathBuf::from(r"C:\temp\test_cleaning");
crate::fs::tokio::reset_dir(&dir).await?;
Git.init(&dir)?.run_ok().await?;
Git.init(&dir).await?;
let foo = dir.join("foo");
let foo_target = foo.join("target");

View File

@ -0,0 +1,263 @@
//! Customized formatting utilities.
//!
//! See:
//! <https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt>
use crate::prelude::*;
/// Placeholders that can be used with `format:<...>` in git commands.
/// See: <https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt>
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Placeholder {
// === Placeholders that expand to a single literal ===
/// A newline character.
Newline,
/// A raw percent sign.
Percent,
/// Hex-encoded byte character.
HexCode(u8),
// === Placeholders that control formatting ===
// TODO: Add, as needed.
// === Placeholders for commit information ===
/// The commit hash.
Hash,
/// Abbreviated commit hash.
AbbreviatedHash,
/// Tree hash.
TreeHash,
/// Abbreviated tree hash.
AbbreviatedTreeHash,
/// Parent hashes.
ParentHashes,
/// Abbreviated parent hashes.
AbbreviatedParentHashes,
/// Author name.
AuthorName,
/// Author name respecting `.mailmap`.
AuthorNameMailmap,
/// Author email.
AuthorEmail,
/// Author email respecting `.mailmap`.
AuthorEmailMailmap,
/// Author email local part (the part before the @).
AuthorEmailLocal,
/// Author email local part respecting `.mailmap`.
AuthorEmailLocalMailmap,
/// Author date, respecting the --date=<format> option.
AuthorDate,
/// Author date in [RFC 2822 format](https://www.rfc-editor.org/rfc/rfc2822).
AuthorDateRfc2822,
/// Author date, relative to current time (e.g. "2 weeks ago").
AuthorDateRelative,
/// Author date in UNIX timestamp format.
AuthorDateUnixTimestamp,
/// Author date, ISO 8601-like format. The differences to the strict ISO 8601 format are:
/// * a space instead of the T date/time delimiter;
/// * a space between time and time zone;
/// * no colon between hours and minutes of the time zone.
AuthorDateIso8601Like,
/// Author date, strict ISO 8601 format.
AuthorDateIso8601Strict,
/// Author date, short format (YYYY-MM-DD).
AuthorDateShort,
/// Author date, human-friendly format.
///
/// This shows the timezone if the timezone does not match the current time-zone, and doesn't
/// print the whole date if that matches (ie skip printing year for dates that are "this year",
/// but also skip the whole date itself if its in the last few days and we can just say what
/// weekday it was). For older dates the hour and minute is also omitted.
AuthorDateHuman,
/// Committer name.
CommitterName,
/// Committer name respecting `.mailmap`.
CommitterNameMailmap,
/// Committer email.
CommitterEmail,
/// Committer email respecting `.mailmap`.
CommitterEmailMailmap,
/// Committer email local part (the part before the @).
CommitterEmailLocal,
/// Committer email local part respecting `.mailmap`.
CommitterEmailLocalMailmap,
/// Committer date, respecting the `--date=<format>` option.
CommitterDate,
/// Committer date in [RFC 2822 format](https://www.rfc-editor.org/rfc/rfc2822).
CommitterDateRfc2822,
/// Committer date, relative to current time (e.g. "2 weeks ago").
CommitterDateRelative,
/// Committer date in UNIX timestamp format.
CommitterDateUnixTimestamp,
/// Committer date, ISO 8601-like format. The differences to the strict ISO 8601 format are:
/// * a space instead of the T date/time delimiter;
/// * a space between time and time zone;
/// * no colon between hours and minutes of the time zone.
CommitterDateIso8601Like,
/// Committer date, strict ISO 8601 format.
CommitterDateIso8601Strict,
/// Committer date, short format (YYYY-MM-DD).
CommitterDateShort,
/// Committer date, human-friendly format.
///
/// This shows the timezone if the timezone does not match the current time-zone, and doesnt
/// print the whole date if that matches (ie skip printing year for dates that are "this year",
/// but also skip the whole date itself if its in the last few days and we can just say what
/// weekday it was). For older dates the hour and minute is also omitted.
CommitterDateHuman,
/// Ref names, like the --decorate option of [git-log](https://git-scm.com/docs/git-log).
RefNamesParenthesized,
/// Ref names without the parentheses wrapping.
RefNames,
/// Ref name that was used to find the commit. Works only with `git-log`.
RefNameUsed,
/// Encoding name.
Encoding,
/// Subject. This is the commit message without the body.
Subject,
/// Sanitized subject line, suitable for a filename.
SubjectSanitized,
/// Body. This is the commit message without the subject line.
Body,
/// Raw body.
RawBody,
/// Commit notes.
Notes,
/// Raw verification message from GPG for a signed commit.
GpgVerificationMessage,
/// GPG verification status for a signed commit.
///
/// Show "G" for a good (valid) signature, "B" for a bad signature, "U" for a good signature
/// with unknown validity, "X" for a good signature that has expired, "Y" for a good signature
/// made by an expired key, "R" for a good signature made by a revoked key, "E" if the
/// signature cannot be checked (e.g. missing key) and "N" for no signature.
GpgVerificationStatus,
/// The name of the signer of a signed commit.
GpgSignerName,
/// The key used to sign a signed commit.
GpgSignerKey,
/// The fingerprint of the key used to sign a signed commit.
GpgSignerFingerprint,
/// The fingerprint of the primary key whose subkey was used to sign a signed commit.
GpgSignerPrimaryFingerprint,
/// Trust level of the key used to sign a signed commit.
GpgSignerTrustLevel,
/// Reflog selector.
ReflogSelector,
/// Shortened reflog selector.
ReflogSelectorShort,
/// Reflog identity name.
ReflogIdentity,
/// Reflog identity mail respecting `.mailmap`.
ReflogIdentityMailmap,
/// Reflog identity email.
ReflogIdentityEmail,
/// Reflog identity email respecting `.mailmap`.
ReflogIdentityEmailMailmap,
/// Reflog subject.
ReflogSubject,
// TODO: trailers of the body
// https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emtrailersoptionsem
}
impl Display for Placeholder {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Placeholder::Newline => write!(f, "%n"),
Placeholder::Percent => write!(f, "%%"),
Placeholder::HexCode(code) => write!(f, "%{:02x}", code),
Placeholder::Hash => write!(f, "%H"),
Placeholder::AbbreviatedHash => write!(f, "%h"),
Placeholder::TreeHash => write!(f, "%T"),
Placeholder::AbbreviatedTreeHash => write!(f, "%t"),
Placeholder::ParentHashes => write!(f, "%P"),
Placeholder::AbbreviatedParentHashes => write!(f, "%p"),
Placeholder::AuthorName => write!(f, "%an"),
Placeholder::AuthorNameMailmap => write!(f, "%aN"),
Placeholder::AuthorEmail => write!(f, "%ae"),
Placeholder::AuthorEmailMailmap => write!(f, "%aE"),
Placeholder::AuthorEmailLocal => write!(f, "%al"),
Placeholder::AuthorEmailLocalMailmap => write!(f, "%aL"),
Placeholder::AuthorDate => write!(f, "%ad"),
Placeholder::AuthorDateRfc2822 => write!(f, "%aD"),
Placeholder::AuthorDateRelative => write!(f, "%ar"),
Placeholder::AuthorDateUnixTimestamp => write!(f, "%at"),
Placeholder::AuthorDateIso8601Like => write!(f, "%ai"),
Placeholder::AuthorDateIso8601Strict => write!(f, "%aI"),
Placeholder::AuthorDateShort => write!(f, "%as"),
Placeholder::AuthorDateHuman => write!(f, "%ah"),
Placeholder::CommitterName => write!(f, "%cn"),
Placeholder::CommitterNameMailmap => write!(f, "%cN"),
Placeholder::CommitterEmail => write!(f, "%ce"),
Placeholder::CommitterEmailMailmap => write!(f, "%cE"),
Placeholder::CommitterEmailLocal => write!(f, "%cl"),
Placeholder::CommitterEmailLocalMailmap => write!(f, "%cL"),
Placeholder::CommitterDate => write!(f, "%cd"),
Placeholder::CommitterDateRfc2822 => write!(f, "%cD"),
Placeholder::CommitterDateRelative => write!(f, "%cr"),
Placeholder::CommitterDateUnixTimestamp => write!(f, "%ct"),
Placeholder::CommitterDateIso8601Like => write!(f, "%ci"),
Placeholder::CommitterDateIso8601Strict => write!(f, "%cI"),
Placeholder::CommitterDateShort => write!(f, "%cs"),
Placeholder::CommitterDateHuman => write!(f, "%ch"),
Placeholder::RefNamesParenthesized => write!(f, "%d"),
Placeholder::RefNames => write!(f, "%D"),
Placeholder::RefNameUsed => write!(f, "%S"),
Placeholder::Encoding => write!(f, "%e"),
Placeholder::Subject => write!(f, "%s"),
Placeholder::SubjectSanitized => write!(f, "%f"),
Placeholder::Body => write!(f, "%b"),
Placeholder::RawBody => write!(f, "%B"),
Placeholder::Notes => write!(f, "%N"),
Placeholder::GpgVerificationMessage => write!(f, "%GG"),
Placeholder::GpgVerificationStatus => write!(f, "%G?"),
Placeholder::GpgSignerName => write!(f, "%GS"),
Placeholder::GpgSignerKey => write!(f, "%GK"),
Placeholder::GpgSignerFingerprint => write!(f, "%GF"),
Placeholder::GpgSignerPrimaryFingerprint => write!(f, "%GP"),
Placeholder::GpgSignerTrustLevel => write!(f, "%GT"),
Placeholder::ReflogSelector => write!(f, "%gD"),
Placeholder::ReflogSelectorShort => write!(f, "%gd"),
Placeholder::ReflogIdentity => write!(f, "%gn"),
Placeholder::ReflogIdentityMailmap => write!(f, "%gN"),
Placeholder::ReflogIdentityEmail => write!(f, "%ge"),
Placeholder::ReflogIdentityEmailMailmap => write!(f, "%gE"),
Placeholder::ReflogSubject => write!(f, "%gs"),
}
}
}
/// Get reference names from the decoration printed from `git log` with `--decorate=full` and
/// `%D` format ([Placeholder::RefNames]).
///
/// Such names can be further parsed, e.g. into [`Ref`].
///
/// ```
/// use ide_ci::programs::git::pretty_format::refs_from_decoration;
/// let text = "HEAD -> refs/heads/wip/mwu/stable-release, tag: refs/tags/2022.1.1-nightly.2022-11-09, refs/heads/develop";
/// let refs = refs_from_decoration(text);
/// assert_eq!(refs, vec![
/// "HEAD",
/// "refs/heads/wip/mwu/stable-release",
/// "refs/tags/2022.1.1-nightly.2022-11-09",
/// "refs/heads/develop"
/// ]);
/// ```
pub fn refs_from_decoration(text: &str) -> Vec<&str> {
text.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.flat_map(|s| {
if let Some(stripped) = s.strip_prefix("HEAD -> ") {
vec!["HEAD", stripped]
} else {
vec![s]
}
.into_iter()
})
.map(|s| s.trim_start_matches("tag: "))
.collect_vec()
}

View File

@ -0,0 +1,116 @@
//! Code for representing git references.
//!
//! See [Git Reference Documentation](https://git-scm.com/book/en/v2/Git-Internals-Git-References).
use crate::prelude::*;
/// A git reference.
///
/// It is a name that points to an object in the repository or another reference.
///
/// # Examples
/// ```
/// use enso_build_base::prelude::FromString;
/// use ide_ci::programs::git::Ref;
/// let reference = Ref::from_str("refs/heads/master").unwrap();
/// assert_eq!(reference, Ref::Branch { name: "master".into() });
/// ```
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Ref {
/// The current branch.
///
/// HEAD is a reference to one of the heads in your repository, except when using a detached
/// HEAD, in which case it directly references an arbitrary commit.
Head,
/// Head of a local branch.
Branch {
/// Branch name, stripped of the `refs/heads/` prefix.
name: String,
},
/// Head of a remote branch.
RemoteBranch {
/// Name of the remote.
remote: String,
/// Branch name, stripped of the `refs/remotes/<remote>/` prefix.
branch: String,
},
/// A ref under refs/tags/ namespace that points to an object of an arbitrary type (typically a
/// tag points to either a tag or a commit object).
Tag {
/// Name of the tag. Any trailing `^{}` suffix is stripped.
name: String,
/// Whether the tag was peeled, i.e. had `^{}` suffix.
peeled: bool,
},
/// All other refs, e.g. `refs/notes/commits` or `MERGE_HEAD`.
Other {
/// Full reference name.
name: String,
},
}
impl std::str::FromStr for Ref {
type Err = anyhow::Error;
/// Parse the reference from the full decorated name, like `refs/heads/main`.
/// See: <https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefdecorateadecorate>
///
/// # Examples
/// ```
/// use enso_build_base::prelude::*;
/// use ide_ci::programs::git::r#ref::Ref;
/// fn main() -> Result {
/// assert_eq!(Ref::from_str("HEAD")?, Ref::Head);
/// assert_eq!(Ref::from_str("refs/heads/main")?, Ref::Branch { name: "main".into() });
/// assert_eq!(Ref::from_str("refs/remotes/origin/main")?, Ref::RemoteBranch {
/// remote: "origin".into(),
/// branch: "main".into(),
/// });
/// assert_eq!(Ref::from_str("refs/tags/2022.1.1-nightly.2022-11-09")?, Ref::Tag {
/// name: "2022.1.1-nightly.2022-11-09".into(),
/// peeled: false,
/// });
/// assert_eq!(Ref::from_str("refs/tags/2022.1.1-nightly.2022-11-09^{}")?, Ref::Tag {
/// name: "2022.1.1-nightly.2022-11-09".into(),
/// peeled: true,
/// });
/// Ok(())
/// }
/// ```
fn from_str(name: &str) -> Result<Self> {
let name = name.trim();
Ok(if name == "HEAD" {
Ref::Head
} else if let Some(name) = name.strip_prefix("refs/heads/") {
Ref::Branch { name: name.to_string() }
} else if let Some(name) = name.strip_prefix("refs/remotes/") {
let mut parts = name.splitn(2, '/');
let remote = parts.next().context("Missing remote name.")?.to_string();
let branch = parts.next().context("Missing branch name.")?.to_string();
Ref::RemoteBranch { remote, branch }
} else if let Some(name) = name.strip_prefix("refs/tags/") {
if let Some(peeled_name) = name.strip_suffix("^{}") {
Ref::Tag { name: peeled_name.to_string(), peeled: true }
} else {
Ref::Tag { name: name.to_string(), peeled: false }
}
} else {
bail!("Unknown ref name: `{}`.", name);
})
}
}
impl Display for Ref {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Ref::Head => write!(f, "HEAD"),
Ref::Branch { name } => write!(f, "refs/heads/{name}"),
Ref::RemoteBranch { remote, branch } => write!(f, "refs/remotes/{remote}/{branch}"),
Ref::Tag { name, peeled } =>
write!(f, "refs/tags/{name}{}", if *peeled { "^{}" } else { "" }),
Ref::Other { name } => write!(f, "{name}"),
}
}
}

View File

@ -160,10 +160,6 @@ pub struct Cli {
#[clap(long, global = true, default_value_t = default_repo_remote(), enso_env())]
pub repo_remote: Repo,
/// The build kind. Affects the default version generation.
#[clap(long, global = true, arg_enum, default_value_t = enso_build::version::Kind::Dev, env = *crate::ENSO_BUILD_KIND)]
pub build_kind: enso_build::version::Kind,
/// Platform to target. Currently cross-compilation is enabled only for GUI/IDE (without
/// Project Manager) on platforms where Electron Builder supports this.
#[clap(long, global = true, default_value_t = TARGET_OS, enso_env(), possible_values=[OS::Windows.as_str(), OS::Linux.as_str(), OS::MacOS.as_str()])]

View File

@ -14,14 +14,24 @@ pub struct DeployRuntime {
#[derive(Args, Clone, Copy, Debug)]
pub struct DeployGui {}
/// Structure that represents `promote` subcommand arguments.
#[derive(Args, Clone, Copy, Debug)]
pub struct Promote {
/// What kind of version is to be created.
#[clap(arg_enum)]
pub designation: enso_build::version::promote::Designation,
}
#[derive(Subcommand, Clone, Debug)]
pub enum Action {
/// Create a release draft on GitHub.
CreateDraft,
/// Build the runtime image and push it to ECR.
DeployRuntime(DeployRuntime),
/// Upload the GUI to the S3 Bucket and notify.
DeployGui(DeployGui),
Publish,
Promote(Promote),
}
#[derive(Args, Clone, Debug)]

View File

@ -7,20 +7,30 @@
use enso_build_cli::prelude::*;
use enso_build::setup_octocrab;
use ide_ci::github::Repo;
use ide_ci::github::RepoRef;
use ide_ci::io::web::handle_error_response;
use ide_ci::log::setup_logging;
const REPO: RepoRef = RepoRef { owner: "enso-org", name: "enso" };
#[tokio::main]
async fn main() -> Result {
setup_logging()?;
info!("Removing draft releases from GitHub.");
let octo = setup_octocrab().await?;
let repo = Repo::from_str("enso-org/enso")?.handle(&octo);
if let Err(e) = octo.current().user().await {
bail!(
"Failed to authenticate: {}.\nBeing authenticated is necessary to see draft releases.",
e
);
}
let repo = REPO.handle(&octo);
info!("Fetching all releases.");
let releases = repo.all_releases().await?;
let draft_releases = releases.into_iter().filter(|r| r.draft);
let draft_releases = releases.into_iter().filter(|r| r.draft).collect_vec();
info!("Found {} draft releases.", draft_releases.len());
for release in draft_releases {
let id = release.id;
@ -30,6 +40,6 @@ async fn main() -> Result {
handle_error_response(response).await?;
}
info!("Done.");
Ok(())
}

View File

@ -5,7 +5,10 @@ use crate::ci_gen::job::plain_job;
use crate::ci_gen::job::plain_job_customized;
use crate::ci_gen::job::RunsOn;
use enso_build::version;
use enso_build::version::promote::Designation;
use enso_build::version::ENSO_EDITION;
use enso_build::version::ENSO_RELEASE_MODE;
use enso_build::version::ENSO_VERSION;
use ide_ci::actions::workflow::definition::checkout_repo_step;
use ide_ci::actions::workflow::definition::is_non_windows_runner;
use ide_ci::actions::workflow::definition::is_windows_runner;
@ -19,6 +22,7 @@ use ide_ci::actions::workflow::definition::Concurrency;
use ide_ci::actions::workflow::definition::Event;
use ide_ci::actions::workflow::definition::Job;
use ide_ci::actions::workflow::definition::JobArchetype;
use ide_ci::actions::workflow::definition::JobSecrets;
use ide_ci::actions::workflow::definition::PullRequest;
use ide_ci::actions::workflow::definition::PullRequestActivityType;
use ide_ci::actions::workflow::definition::Push;
@ -26,9 +30,11 @@ use ide_ci::actions::workflow::definition::RunnerLabel;
use ide_ci::actions::workflow::definition::Schedule;
use ide_ci::actions::workflow::definition::Step;
use ide_ci::actions::workflow::definition::Workflow;
use ide_ci::actions::workflow::definition::WorkflowCall;
use ide_ci::actions::workflow::definition::WorkflowDispatch;
use ide_ci::actions::workflow::definition::WorkflowDispatchInput;
use ide_ci::actions::workflow::definition::WorkflowDispatchInputType;
use strum::IntoEnumIterator;
// ==============
@ -54,6 +60,12 @@ pub const DEFAULT_BRANCH_NAME: &str = "develop";
pub const RELEASE_CONCURRENCY_GROUP: &str = "release";
pub const DESIGNATOR_INPUT_NAME: &str = "designator";
pub const PROMOTE_WORKFLOW_PATH: &str = "./.github/workflows/promote.yml";
pub const RELEASE_WORKFLOW_PATH: &str = "./.github/workflows/release.yml";
/// Secrets set up in our organization.
///
/// To manage, see: https://github.com/organizations/enso-org/settings/secrets/actions
@ -92,6 +104,12 @@ pub fn release_concurrency() -> Concurrency {
Concurrency::new(RELEASE_CONCURRENCY_GROUP)
}
/// Get expression that gets input from the workflow dispatch. See:
/// <https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#providing-inputs>
pub fn get_input_expression(name: impl Into<String>) -> String {
wrap_expression(format!("inputs.{}", name.into()))
}
impl RunsOn for DeluxeRunner {
fn runs_on(&self) -> Vec<RunnerLabel> {
vec![RunnerLabel::MwuDeluxe]
@ -179,20 +197,19 @@ pub fn setup_script_and_steps(command_line: impl AsRef<str>) -> Vec<Step> {
#[derive(Clone, Copy, Debug)]
pub struct DraftRelease;
impl JobArchetype for DraftRelease {
fn job(os: OS) -> Job {
let name = "Create release draft".into();
fn job(&self, os: OS) -> Job {
let name = "Create a release draft.".into();
let prepare_step = run("release create-draft").with_id(Self::PREPARE_STEP_ID);
let mut steps = setup_script_steps();
steps.push(prepare_step);
let mut ret = Job { name, runs_on: runs_on(os), steps, ..default() };
Self::expose_outputs(&mut ret);
self.expose_outputs(&mut ret);
ret
}
fn outputs() -> BTreeMap<String, Vec<String>> {
fn outputs(&self) -> BTreeMap<String, Vec<String>> {
let mut ret = BTreeMap::new();
ret.insert(Self::PREPARE_STEP_ID.into(), vec![
"ENSO_VERSION".into(),
@ -209,7 +226,7 @@ impl DraftRelease {
#[derive(Clone, Copy, Debug)]
pub struct PublishRelease;
impl JobArchetype for PublishRelease {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
let mut ret = plain_job(&os, "Publish release", "release publish");
ret.expose_secret_as(secret::ARTEFACT_S3_ACCESS_KEY_ID, "AWS_ACCESS_KEY_ID");
ret.expose_secret_as(secret::ARTEFACT_S3_SECRET_ACCESS_KEY, "AWS_SECRET_ACCESS_KEY");
@ -221,13 +238,39 @@ impl JobArchetype for PublishRelease {
#[derive(Clone, Copy, Debug)]
pub struct UploadIde;
impl JobArchetype for UploadIde {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job_customized(&os, "Build IDE", "ide upload --wasm-source current-ci-run --backend-source release --backend-release ${{env.ENSO_RELEASE_ID}}", |step|
vec![expose_os_specific_signing_secret(os, step)]
)
}
}
#[derive(Clone, Copy, Debug)]
pub struct PromoteReleaseJob;
impl JobArchetype for PromoteReleaseJob {
fn job(&self, os: OS) -> Job {
let command = format!("release promote {}", get_input_expression(DESIGNATOR_INPUT_NAME));
let mut job = plain_job_customized(&os, "Promote release", command, |step| {
vec![step.with_id(Self::PROMOTE_STEP_ID)]
});
self.expose_outputs(&mut job);
job
}
fn outputs(&self) -> BTreeMap<String, Vec<String>> {
let mut ret = BTreeMap::new();
ret.insert(Self::PROMOTE_STEP_ID.into(), vec![
ENSO_VERSION.name.to_string(),
ENSO_EDITION.name.to_string(),
ENSO_RELEASE_MODE.name.to_string(),
]);
ret
}
}
impl PromoteReleaseJob {
pub const PROMOTE_STEP_ID: &'static str = "promote";
}
/// Generate a workflow that checks if the changelog has been updated (if needed).
pub fn changelog() -> Result<Workflow> {
use PullRequestActivityType::*;
@ -256,27 +299,24 @@ pub fn nightly() -> Result<Workflow> {
..default()
};
let mut workflow = Workflow {
on,
name: "Nightly Release".into(),
concurrency: Some(release_concurrency()),
..default()
};
let mut workflow = Workflow { on, name: "Nightly Release".into(), ..default() };
add_release_steps(&mut workflow, version::Kind::Nightly)?;
let job = workflow_call_job("Promote nightly", PROMOTE_WORKFLOW_PATH)
.with_with(DESIGNATOR_INPUT_NAME, Designation::Nightly.as_ref());
workflow.add_job(job);
Ok(workflow)
}
fn add_release_steps(workflow: &mut Workflow, kind: version::Kind) -> Result {
let prepare_job_id = workflow.add::<DraftRelease>(PRIMARY_OS);
let build_wasm_job_id = workflow.add::<job::BuildWasm>(PRIMARY_OS);
fn add_release_steps(workflow: &mut Workflow) -> Result {
let prepare_job_id = workflow.add(PRIMARY_OS, DraftRelease);
let build_wasm_job_id = workflow.add(PRIMARY_OS, job::BuildWasm);
let mut packaging_job_ids = vec![];
// Assumed, because Linux is necessary to deploy ECR runtime image.
assert!(TARGETED_SYSTEMS.contains(&OS::Linux));
for os in TARGETED_SYSTEMS {
let backend_job_id = workflow.add_dependent::<job::UploadBackend>(os, [&prepare_job_id]);
let build_ide_job_id = workflow.add_dependent::<UploadIde>(os, [
let backend_job_id = workflow.add_dependent(os, job::UploadBackend, [&prepare_job_id]);
let build_ide_job_id = workflow.add_dependent(os, UploadIde, [
&prepare_job_id,
&backend_job_id,
&build_wasm_job_id,
@ -289,11 +329,11 @@ fn add_release_steps(workflow: &mut Workflow, kind: version::Kind) -> Result {
if os == OS::Linux {
let runtime_requirements = [&prepare_job_id, &backend_job_id];
let upload_runtime_job_id =
workflow.add_dependent::<job::DeployRuntime>(os, runtime_requirements);
workflow.add_dependent(os, job::DeployRuntime, runtime_requirements);
packaging_job_ids.push(upload_runtime_job_id);
let gui_requirements = [build_ide_job_id];
let deploy_gui_job_id = workflow.add_dependent::<job::DeployGui>(os, gui_requirements);
let deploy_gui_job_id = workflow.add_dependent(os, job::DeployGui, gui_requirements);
packaging_job_ids.push(deploy_gui_job_id);
}
}
@ -304,27 +344,76 @@ fn add_release_steps(workflow: &mut Workflow, kind: version::Kind) -> Result {
};
let _publish_job_id = workflow.add_dependent::<PublishRelease>(PRIMARY_OS, publish_deps);
let global_env = [(*crate::ENSO_BUILD_KIND, kind.as_ref()), ("RUST_BACKTRACE", "full")];
for (var_name, value) in global_env {
workflow.env(var_name, value);
}
let _publish_job_id = workflow.add_dependent(PRIMARY_OS, PublishRelease, publish_deps);
workflow.env("RUST_BACKTRACE", "full");
Ok(())
}
pub fn release_candidate() -> Result<Workflow> {
let on = Event { workflow_dispatch: Some(default()), ..default() };
pub fn workflow_call_job(name: impl Into<String>, path: impl Into<String>) -> Job {
Job {
name: name.into(),
uses: Some(path.into()),
secrets: Some(JobSecrets::Inherit),
..default()
}
}
pub fn call_release_job(version_input_expr: &str) -> Job {
workflow_call_job("Release", RELEASE_WORKFLOW_PATH).with_with("version", version_input_expr)
}
pub fn release() -> Result<Workflow> {
let version_input = WorkflowDispatchInput::new_string(
"What version number this release should get.",
true,
None::<String>,
);
let workflow_dispatch = WorkflowDispatch::default().with_input("version", version_input);
let workflow_call = WorkflowCall::try_from(workflow_dispatch.clone())?;
let on = Event {
workflow_dispatch: Some(workflow_dispatch),
workflow_call: Some(workflow_call),
..default()
};
let mut workflow = Workflow {
on,
name: "Release Candidate".into(),
name: "Release".into(),
concurrency: Some(release_concurrency()),
..default()
};
add_release_steps(&mut workflow, version::Kind::Rc)?;
add_release_steps(&mut workflow)?;
let version_input_expression = get_input_expression("version");
workflow.env(ENSO_EDITION.name, &version_input_expression);
workflow.env(ENSO_VERSION.name, &version_input_expression);
Ok(workflow)
}
pub fn promote() -> Result<Workflow> {
let designator = WorkflowDispatchInput::new_choice(
"What kind of release should be promoted.",
true,
Designation::iter().map(|d| d.as_ref().to_string()),
None::<String>,
)?;
let workflow_dispatch =
WorkflowDispatch::default().with_input(DESIGNATOR_INPUT_NAME, designator);
let on = Event {
workflow_call: Some(WorkflowCall::try_from(workflow_dispatch.clone())?),
workflow_dispatch: Some(workflow_dispatch),
..default()
};
let mut workflow = Workflow { on, name: "Generate a new version".into(), ..default() };
let promote_job_id = workflow.add(PRIMARY_OS, PromoteReleaseJob);
let version_input = format!("needs.{promote_job_id}.outputs.{ENSO_VERSION}");
let mut release_job = call_release_job(&wrap_expression(version_input));
release_job.needs(&promote_job_id);
workflow.add_job(release_job);
Ok(workflow)
}
@ -340,10 +429,10 @@ pub fn typical_check_triggers() -> Event {
pub fn gui() -> Result<Workflow> {
let on = typical_check_triggers();
let mut workflow = Workflow { name: "GUI CI".into(), on, ..default() };
workflow.add::<job::CancelWorkflow>(PRIMARY_OS);
workflow.add::<job::Lint>(PRIMARY_OS);
workflow.add::<job::WasmTest>(PRIMARY_OS);
workflow.add::<job::NativeTest>(PRIMARY_OS);
workflow.add(PRIMARY_OS, job::CancelWorkflow);
workflow.add(PRIMARY_OS, job::Lint);
workflow.add(PRIMARY_OS, job::WasmTest);
workflow.add(PRIMARY_OS, job::NativeTest);
// FIXME: Integration tests are currently always failing.
// The should be reinstated when fixed.
@ -353,14 +442,14 @@ pub fn gui() -> Result<Workflow> {
// Because WASM upload happens only for the Linux build, all other platforms needs to depend on
// it.
let wasm_job_linux = workflow.add::<job::BuildWasm>(OS::Linux);
let wasm_job_linux = workflow.add(OS::Linux, job::BuildWasm);
for os in TARGETED_SYSTEMS {
if os != OS::Linux {
// Linux was already added above.
let _wasm_job = workflow.add::<job::BuildWasm>(os);
let _wasm_job = workflow.add(os, job::BuildWasm);
}
let project_manager_job = workflow.add::<job::BuildBackend>(os);
workflow.add_customized::<job::PackageIde>(os, |job| {
let project_manager_job = workflow.add(os, job::BuildBackend);
workflow.add_customized(os, job::PackageIde, |job| {
job.needs.insert(wasm_job_linux.clone());
job.needs.insert(project_manager_job);
});
@ -371,9 +460,9 @@ pub fn gui() -> Result<Workflow> {
pub fn backend() -> Result<Workflow> {
let on = typical_check_triggers();
let mut workflow = Workflow { name: "Engine CI".into(), on, ..default() };
workflow.add::<job::CancelWorkflow>(PRIMARY_OS);
workflow.add(PRIMARY_OS, job::CancelWorkflow);
for os in TARGETED_SYSTEMS {
workflow.add::<job::CiCheckBackend>(os);
workflow.add(os, job::CiCheckBackend);
}
Ok(workflow)
}
@ -413,6 +502,7 @@ pub fn generate(repo_root: &enso_build::paths::generated::RepoRootGithubWorkflow
repo_root.scala_new_yml.write_as_yaml(&backend()?)?;
repo_root.gui_yml.write_as_yaml(&gui()?)?;
repo_root.benchmark_yml.write_as_yaml(&benchmark()?)?;
repo_root.release_yml.write_as_yaml(&release_candidate()?)?;
repo_root.release_yml.write_as_yaml(&release()?)?;
repo_root.promote_yml.write_as_yaml(&promote()?)?;
Ok(())
}

View File

@ -70,7 +70,7 @@ pub fn plain_job_customized(
#[derive(Clone, Copy, Debug)]
pub struct CancelWorkflow;
impl JobArchetype for CancelWorkflow {
fn job(_os: OS) -> Job {
fn job(&self, _os: OS) -> Job {
Job {
name: "Cancel Previous Runs".into(),
// It is important that this particular job runs pretty much everywhere (we use x64,
@ -87,7 +87,7 @@ impl JobArchetype for CancelWorkflow {
#[derive(Clone, Copy, Debug)]
pub struct Lint;
impl JobArchetype for Lint {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job(&os, "Lint", "lint")
}
}
@ -95,7 +95,7 @@ impl JobArchetype for Lint {
#[derive(Clone, Copy, Debug)]
pub struct NativeTest;
impl JobArchetype for NativeTest {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job(&os, "Native GUI tests", "wasm test --no-wasm")
}
}
@ -103,7 +103,7 @@ impl JobArchetype for NativeTest {
#[derive(Clone, Copy, Debug)]
pub struct WasmTest;
impl JobArchetype for WasmTest {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job(&os, "WASM GUI tests", "wasm test --no-native")
}
}
@ -111,7 +111,7 @@ impl JobArchetype for WasmTest {
#[derive(Clone, Copy, Debug)]
pub struct IntegrationTest;
impl JobArchetype for IntegrationTest {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job(
&os,
"IDE integration tests",
@ -123,7 +123,7 @@ impl JobArchetype for IntegrationTest {
#[derive(Clone, Copy, Debug)]
pub struct BuildWasm;
impl JobArchetype for BuildWasm {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job(
&os,
"Build GUI (WASM)",
@ -135,7 +135,7 @@ impl JobArchetype for BuildWasm {
#[derive(Clone, Copy, Debug)]
pub struct BuildBackend;
impl JobArchetype for BuildBackend {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job(&os, "Build Backend", "backend get")
}
}
@ -143,7 +143,7 @@ impl JobArchetype for BuildBackend {
#[derive(Clone, Copy, Debug)]
pub struct UploadBackend;
impl JobArchetype for UploadBackend {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job(&os, "Upload Backend", "backend upload")
}
}
@ -151,7 +151,7 @@ impl JobArchetype for UploadBackend {
#[derive(Clone, Copy, Debug)]
pub struct DeployRuntime;
impl JobArchetype for DeployRuntime {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job_customized(&os, "Upload Runtime to ECR", "release deploy-runtime", |step| {
let step = step
.with_secret_exposed_as("CI_PRIVATE_TOKEN", "GITHUB_TOKEN")
@ -170,7 +170,7 @@ impl JobArchetype for DeployRuntime {
#[derive(Clone, Copy, Debug)]
pub struct DeployGui;
impl JobArchetype for DeployGui {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job_customized(&os, "Upload GUI to S3", "release deploy-gui", |step| {
let step = step
.with_secret_exposed_as("CI_PRIVATE_TOKEN", "GITHUB_TOKEN")
@ -220,7 +220,7 @@ pub fn expose_os_specific_signing_secret(os: OS, step: Step) -> Step {
#[derive(Clone, Copy, Debug)]
pub struct PackageIde;
impl JobArchetype for PackageIde {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job_customized(
&os,
"Package IDE",
@ -233,7 +233,7 @@ impl JobArchetype for PackageIde {
#[derive(Clone, Copy, Debug)]
pub struct CiCheckBackend;
impl JobArchetype for CiCheckBackend {
fn job(os: OS) -> Job {
fn job(&self, os: OS) -> Job {
plain_job_customized(&os, "Engine", "backend ci-check", |main_step| {
vec![main_step, step::engine_test_reporter(os), step::stdlib_test_reporter(os)]
})

View File

@ -75,6 +75,7 @@ use enso_build::source::ReleaseSource;
use enso_build::source::Source;
use enso_build::source::WatchTargetJob;
use enso_build::source::WithDestination;
use enso_build::version;
use futures_util::future::try_join;
use ide_ci::actions::workflow::is_in_env;
use ide_ci::cache::Cache;
@ -83,6 +84,7 @@ use ide_ci::fs::remove_if_exists;
use ide_ci::global;
use ide_ci::ok_ready_boxed;
use ide_ci::programs::cargo;
use ide_ci::programs::git;
use ide_ci::programs::git::clean;
use ide_ci::programs::rustc;
use ide_ci::programs::Cargo;
@ -121,13 +123,9 @@ impl Processor {
pub async fn new(cli: &Cli) -> Result<Self> {
let absolute_repo_path = cli.repo_path.absolutize()?;
let octocrab = setup_octocrab().await?;
let remote_repo = cli.repo_remote.handle(&octocrab);
let versions = enso_build::version::deduce_or_generate(
Ok(&remote_repo),
cli.build_kind,
&absolute_repo_path,
)
.await?;
let git = git::new(absolute_repo_path.as_ref()).await?;
let release_provider = || version::promote::releases_on_remote(&git);
let versions = version::deduce_or_generate(release_provider).await?;
let mut triple = TargetTriple::new(versions);
triple.os = cli.target_os;
triple.versions.publish().await?;
@ -845,7 +843,8 @@ pub async fn main_internal(config: Option<enso_build::config::Config>) -> Result
}
Target::Release(release) => match release.action {
Action::CreateDraft => {
enso_build::release::draft_a_new_release(&ctx).await?;
let commit = ide_ci::actions::env::GITHUB_SHA.get()?;
enso_build::release::draft_a_new_release(&ctx, &commit).await?;
}
Action::DeployRuntime(args) => {
enso_build::release::deploy_to_ecr(&ctx, args.ecr_repository).await?;
@ -862,6 +861,10 @@ pub async fn main_internal(config: Option<enso_build::config::Config>) -> Result
Action::Publish => {
enso_build::release::publish_release(&ctx).await?;
}
Action::Promote(args) => {
let crate::arg::release::Promote { designation } = args;
enso_build::release::promote_release(&ctx, designation).await?;
}
},
Target::CiGen => ci_gen::generate(
&enso_build::paths::generated::RepoRootGithubWorkflows::new(cli.repo_path),

View File

@ -160,6 +160,7 @@ mod tests {
use super::*;
#[test]
#[ignore]
fn foo() -> Result {
let code = "enum Foo {
#[arg]
@ -169,10 +170,38 @@ mod tests {
#[arg]
Quux,
}";
let token_stream = syn::parse_str::<TokenStream>(code)?;
let _token_stream = syn::parse_str::<TokenStream>(code)?;
Ok(())
}
/// Structure with AST of parenthesized sequence of assignments.
///
/// For example, `(a = 1, b = ToString::to_string)`.
#[derive(Debug, Clone)]
pub struct Assignments {
pub paren_token: syn::token::Paren,
pub assignments: syn::punctuated::Punctuated<syn::ExprAssign, syn::Token![,]>,
}
dbg!(token_stream);
impl Parse for Assignments {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let content;
let paren_token = syn::parenthesized!(content in input);
let assignments = content.parse_terminated(syn::ExprAssign::parse)?;
Ok(Self { paren_token, assignments })
}
}
#[test]
#[ignore]
fn parse_attribute() -> Result {
let attribute = r#"(format = ToString :: to_string)"#;
let token_stream = syn::parse_str::<TokenStream>(attribute)?;
dbg!(&token_stream);
let foo = syn::parse2::<Assignments>(token_stream)?;
dbg!(foo);
// let attribute = syn::parse2::<syn::Attribute>(token_stream)?;
// dbg!(attribute);
Ok(())
}
}

View File

@ -0,0 +1,23 @@
// use enso_build_base::prelude::*;
//
// use itertools::Itertools;
//
// #[derive(enso_build_macros::Arg)]
// pub enum Foo {
// Bar,
// #[arg(format = ToString::to_string)]
// TaraPon(u32),
// }
//
// #[test]
// fn test_argument_formatting() {
// let bar = Foo::Bar;
// assert_eq!(bar.into_iter().collect_vec(), vec![OsString::from("--bar")]);
//
// let tara_pon = Foo::TaraPon(42);
// assert_eq!(tara_pon.into_iter().collect_vec(), vec![
// OsString::from("--tara-pon"),
// OsString::from("42")
// ]);
// }

View File

@ -22,18 +22,15 @@ This document describes the infrastructure for Enso's automated nightly builds.
The build can be triggered by two possible events:
- automatically, at 4am UTC after each working day (that is, on Tuesday to
- automatically, at 5am 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.
- manually, dispatching the
[nightly.yml](https://github.com/enso-org/enso/actions/workflows/nightly.yml)
workflow.
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.
The nightly build is based off of the state of the `develop` branch at the
moment when it was triggered, unless a different branch has been specified when
triggering the build.
Thanks to
[GitHub's concurrency settings](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#concurrency),
@ -43,48 +40,48 @@ 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.
Build script automatically generates a new version for each nightly build, based
on the current date and previous tags.
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.
The nightly release version is consists of the following parts:
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 nightly build is triggered on the same day it will be
`1.2.3-SNAPSHOT.2021-02-01.1` etc.
- core version (`major.minor.patch`), which is the next stable version after the
last release,
- prerelease suffix (`-nightly.YYYY.MM.DD`), where `YYYY.MM.DD` is the date of
the build,
- optionally, index suffix (`.N`), where `N` is the index of the nightly build
on a given day.
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.
For example, if the last stable release was `2021.1.1`, the next nightly build
will be `2021.2.0-nightly.2021.05.10`.
Nightly releases are periodically removed from the GitHub releases page, so they
should not be used for anything other than testing.
As each release has a default edition associated with it, so do the nightlies.
For the first nightly released on a given date, the edition associated with it
will be named `nightly-2021-02-01`. For subsequent nightly releases on the same
day, a prefix will be added in the same way as with versions. So for example the
second release on that day will be associated with edition
`nightly-2021-02-01.1` etc.
We follow convention that the edition name is the same as the version. This
convention should not be relied upon, as it may change in the future.
## Release Notes
Each PR should update the first, `Enso Next`, section in `RELEASES.md`. These
The release notes for nightly builds are generated automatically from the
template in `build/build/release-body.md`. The template is filled with a number
of placeholders, which are then replaced with the actual values. Please refer to
the file to learn more.
## Changelog
Each PR should update the first, `Enso Next`, section in `CHANGELOG.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 file was modified. However, in some situations there is no need to update
the notes. In such case `CI: No changelog needed` label should be added to the
PR, so the check is skipped.
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 <version> (<date>)`.
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.

View File

@ -45,12 +45,14 @@ Where `a.b.c-tag` is the version string, `a` is the major version, `b`, is the
minor version, `c` is the patch version, and `tag` is additional metadata, the
following hold:
- major version `a` represents the year of the release, e.g. `2020.1.1` is the
first release of 2020.
- Breaking changes to language behaviour or the public API will result in a
major version increase.
minor version increase.
- Addition of functionality in a backwards-compatible manner will result in a
minor version increase.
- Backwards-compatible bug fixes will result in a patch version increase.
- The tag will indicate pre-release or beta versions, and will increase when any
- The tag will indicate pre-release versions, and will increase when any
pre-release change is made. These are not intended to be stable.
### Launcher Versioning
@ -58,162 +60,49 @@ following hold:
The launcher is released alongside Enso releases, so the launcher version is
tied to the Enso version that it is released with.
## Release Branches
A release branch in the Enso repository is a branch prefixed with `release/`.
Release branches obey the following rules:
- One release branch exists per major version, and is named `release/n.x`, where
`n` is the major version, and the rest is literal.
- A release branch must contain _tags_ corresponding to released versions of
Enso. Once a release has been made, no further changes may be made to that
release.
- A tagged release must contain a `RELEASES.md` file that describes the changes
contained in that release.
It should be noted that general development still takes place on the `main`
branch of the repository.
## Release Workflow
Enso does not use release branches, but instead uses tags to mark releases. The
same commit may be tagged multiple times, once for each release that it is a
part of.
Cutting a release for Enso proceeds as follows:
1. If no release branch exists for the current major version, one should be
created.
2. Create a branch called `wip/<initials/release-bump`. On this branch, ensure
that the release notes are up-to-date in `RELEASES.md` (follow the existing
format), and that the new version number and edition name have been set in
`build.sbt`. These new versions usually involve removing `-SNAPSHOT` from the
versions. This version and edition name should _not_ contain `SNAPSHOT`.
3. Open a PR for this branch into `main`.
4. Once the changes have been reviewed, merge the PR into main (getting commit
hash `xxxxxxx`). The message should be `Prepare for the $version release` as
this message has semantic meaning (it's used in the nightly tooling). Just
before merging, remember to notify the team on Discord to suppress any other
merges to the `main` branch until the next step (bumping versions) is
completed.
5. Immediately push a commit to `main` that updates the version and edition in
`build.sbt` to the new snapshot version. If unclear, bump the patch version
by one and append `-SNAPSHOT` (e.g. `0.2.10` becomes `0.2.11-SNAPSHOT`). The
edition name should have the number after the dot increased and `-SNAPSHOT`
appended, so that `2021.3` becomes `2021.4-SNAPSHOT`. The only exception is
when making the first release in a new year, where the first number should be
bumped to the next year and the second number should be set to 1, for example
`2022.1`. The message should be `Bump the snapshot version`.
6. Find the commit hash of the last "Bump the snapshot version" commit. Let's
say this is `yyyyyyy`.
7. Run a `rebase --onto` the release branch from the `yyyyyyy` commit to the
`xxxxxxx` commit. For example:
1. Ensure that the release notes are up to date and that the top header refers
to the version that is being released.
2. Invoke the "Promote Release" workflow, either by:
```
git rebase --onto origin/release/0.x yyyyyyy~1 xxxxxxx
```
- Triggering it using
[web interface](https://github.com/enso-org/enso/actions/workflows/promote.yml);
- Triggering it using [GitHub CLI](https://cli.github.com/). The following
command should be issued from the root of the repository:
```bash
gh workflow run promote.yml -f designator=<designator>
```
where `<designator>` is denotes what kind of release is being made. It can
be one of:
- `stable` - a stable release (bump to minor version);
- `patch` - a patch release (stable release with a bump to patch version);
- `rc` - a release candidate for the next stable release;
- `nightly` - a nightly release.
8. This will put you into a "detached HEAD" state at commit `zzzzzzz`, so you
need to make a new branch: `git branch release-update zzzzzzz`
9. This new branch is a fast-forward merge away from the release branch. Check
out the release branch and then fast-forward merge `release-update` into it.
For example:
The `promote` workflow acts in the following steps:
```
git checkout release/0.x
git merge --ff-only release-update
```
- generate a new version string for the release;
- create a release draft on GitHub;
- build and upload assets for the release on all platforms;
- publish the release on GitHub.
10. As long as the fast-forward proceeds cleanly, you can push the updated
release branch to the origin.
11. Create a tag for the commit at the HEAD of the release branch. It should be
named as above. As the tag is signed, it must contain a message. The message
should be `Enso <version>`. For example:
The final step also tags the released commit with the version string.
```
git tag --sign enso-0.2.11
```
12. Push the tag to the remote (using `git push --follow-tags`). This will start
the release build automatically.
13. CI will create a draft release for this tag, as well as build and upload the
appropriate artefacts. **Do not** create a release for this tag manually.
14. The release notes for the version being released should be copied from the
new section in `RELEASES.md` into the GitHub release description with the
line breaks removed.
15. The title of the release should be `Enso Engine <version>` (e.g.
`Enso Engine 0.2.11`).
16. Once verification has been performed, the release can be published. It
should _not_ be a pre-releases as we reserve these for nightly builds.
### Breaking Release Workflow
If, however, the engine needs to release but the `HEAD` of `main` is not in a
compatible state with the IDE, the process has to differ a little bit. Please
note that the instructions here are more vague than the above, as exactly what
is required may vary based on the situation.
Consider a scenario where there are four new commits since the last release:
`wwwwwww`, `xxxxxxx`, `yyyyyyy`, `zzzzzzz`. The commit `yyyyyyy` contains
breaking changes that are _not yet integrated with the IDE_. Releasing a package
containing that commit (and those that depend on it) would break the IDE, but we
nevertheless want to release as much as possible:
1. If no release branch exists for the current major version, one should be
created.
2. Rebase the commits that are wanted onto the release branch:
```
git rebase --onto origin/release/0.x wwwwwww~1 xxxxxxx
```
3. This will put you into a "detached HEAD" state at commit `aaaaaaa`, so you
need to make a new branch: `git branch release-update aaaaaaa`, whose `HEAD`
commit is the same as `xxxxxxx`.
4. This new branch is a fast-forward merge away from the release branch. Check
out the release branch and then fast-forward merge `release-update` into it.
For example:
```
git checkout release/0.x
git merge --ff-only release-update
```
5. On the release branch, ensure that the release notes are up to date in
`RELEASES.md` (follow the existing format), and that the new version number
and edition name have been set in `build.sbt`. This version and edition name
should _not_ contain `SNAPSHOT`.
6. Once this is done, create a tag for the commit at the HEAD of the release
branch. It should be named as above. The tag message should be
`Enso <version>`. For example:
```
git tag --sign enso-0.2.11
```
7. Push the tag to the remote. This will start the release build.
8. CI will create a draft release for this tag, as well as build and upload the
appropriate artefacts. **Do not** create a release for this tag manually.
9. The release notes for the version being released should be copied from the
new section in `RELEASES.md` into the GitHub release description with the
line breaks removed.
10. The title of the release should be `Enso Engine <version>` (e.g.
`Enso Engine 0.2.11`).
11. Check out the main branch, and then synchronise the changes to `RELEASES.md`
on the release branch with the changes on `main`.
12. In the same commit, Update the build version number in `build.sbt` to the
new snapshot version. If unclear, bump the patch version by one and append
`-SNAPSHOT` (e.g. `0.2.10` becomes `0.2.11-SNAPSHOT`). The edition name
should have the number after the dot increased and `-SNAPSHOT` appended, so
that `2021.3` becomes `2021.4-SNAPSHOT`. The message should be
`Bump the snapshot version`.
13. Push this commit into `origin/main`, or merge via PR if unable to directly
push.
It is recommended that you instigate a freeze on merges to `main` whilst
performing this process.
3. If the release was stable or patch, immediately update the
[changelog](../CHANGELOG.md) by adding a new header for the next release, and
marking the released one with the version generated.
### Tag Naming
Tags for releases are named as follows `enso-version`, where `version` is the
semver string (see [versioning](#versioning)) representing the version being
released.
Tags for releases are named as follows `version`, where `version` is the semver
string (see [versioning](#versioning)) representing the version being released.
### Manifest Files
@ -378,88 +267,9 @@ that will persist the broken mark to S3 is not triggered for release drafts.
> **When marking the release as broken, you should make sure that the workflow
> persisting the broken mark to Se has succeeded and re-run it if necessary.**
### Release Notes
Release notes should contain a summary of the changes made between the last
release and the current release. They should follow the template given below,
and are contained in the `RELEASES.md` file in the repository root.
```md
# Enso x.y.z (YYYY-MM-DD)
## Language
- A list of language-level changes.
## Interpreter/Runtime
- A list of changes to the Enso interpreter.
## Type System
- A list of type-system changes.
## Tooling
- A list of changes to the Enso language tooling.
## Libraries
- A list of changes to the Enso core libraries.
## Stabilised Features
- A list of stabilised APIs and/or features.
## Misc
- A list of miscellaneous changes.
## Internal Only
- A list of changes that do not have user-facing impact, but represent
significant improvements to the internals of Enso and related tools.
```
If there are no changes for a section, the section may be removed.
The releases file is an ongoing record of changes, and may diverge between
`main` and the various release branches.
## Version Support
We aim to support a given major version for some period of time after the
release of the next major version. For a detailed breakdown of the major
versions that are currently supported, please see the [security](./security.md)
document.
## Working on the Current Release
When working on the current release, development should take place against the
`main` branch. When it is time to cut a release, the new commits on the main
branch are cherry-picked onto the current release branch. From there, the
release proceeds as described in [release workflow](#release-workflow) above.
## Backporting Fixes
Supporting a major version for some time after the release of the next major
version will sometimes require backporting a fix to the previous major version
from the current version or from `main`.
Backporting should only be used for applying _fixes_, not the addition of new
features.
The process for performing such a backport is as follows:
1. Create a new branch called `backport/version/fix-name`, where `version`
matches the version string of the corresponding release branch. This branch
should branch off the corresponding release branch.
2. Back-port the fix to the newly created `backport` branch. This can be done
by:
- Cherry-picking the commit and performing fixups (preferred).
- Re-implementing the fix manually (if cherry-picking will not work due to
progression of the codebase).
3. Submit your `backport/version/fix-name` branch for review as a pull-request
into the `release/version` branch.
4. Once the PR has passed CI and been approved by the appropriate reviewers, it
can be merged into the release branch.

View File

@ -87,6 +87,7 @@ public class DebuggingEnsoTest {
* checks the value of the `accumulator` variable.
*/
@Test
@Ignore
public void recursiveFactorialCall() throws Exception {
final URI facUri = new URI("memory://fac.enso");
final Source facSrc = Source.newBuilder("enso", """
@ -125,6 +126,7 @@ public class DebuggingEnsoTest {
* stack frames, including the stack frame of the caller method.
*/
@Test
@Ignore
public void callerVariablesAreVisibleOnPreviousStackFrame() {
URI fooUri = URI.create("memory://tmp.enso");
Source fooSource = Source.newBuilder("enso", """

11
run.ps1
View File

@ -13,8 +13,9 @@ $BuildScriptBin = "enso-build-cli"
$TargetExe = Join-Path $TargetDir $BuildScriptProfile $BuildScriptBin
$BuildArgs = "build", "--profile", $BuildScriptProfile, "--target-dir", $TargetDir, "--package", $BuildScriptBin
Set-Location $PSScriptRoot
Start-Process cargo -NoNewWindow -Wait -ArgumentList $BuildArgs
if (!$?) { Exit $LASTEXITCODE }
Start-Process $TargetExe -NoNewWindow -Wait -ArgumentList $args
Exit $LASTEXITCODE
$BuildScriptProcess = Start-Process cargo -NoNewWindow -PassThru -Wait -WorkingDirectory $PSScriptRoot -ArgumentList $BuildArgs
if ($BuildScriptProcess.ExitCode -ne 0) {
Exit $BuildScriptProcess.ExitCode
}
$BuildScriptBinProcess = Start-Process $TargetExe -NoNewWindow -PassThru -Wait -WorkingDirectory $PSScriptRoot -ArgumentList $args
Exit $BuildScriptBinProcess.ExitCode