diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 46631245f3c4..9dec61fcb449 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,118 +31,100 @@ on: types: - created -# Keep in sync with codeql-analysis.yml and test.yml and analysis-of-endpoint-connections.yml -env: - CI: true - node: 22 - java: 21 - RAW_URL: https://raw.githubusercontent.com/${{ github.repository }}/${{ github.sha }} +# Keep this filename in sync with the filename environment variable (PR_AUTO_BUILD_FILE_NAME) in the testserver-deployment.yml workflow jobs: - build: - name: Build .war artifact + define-inputs: + name: Define Inputs runs-on: ubuntu-latest + outputs: + release_upload: ${{ steps.set-upload-release.outputs.release_upload }} + release_url: ${{ steps.set-upload-release.outputs.release_url }} + release_path: ${{ steps.set-upload-release.outputs.release_path }} + release_name: ${{ steps.set-upload-release.outputs.release_name }} + release_type: ${{ steps.set-upload-release.outputs.release_type }} + docker_build: ${{ steps.set-docker-build.outputs.docker_build }} + docker_ref: ${{ steps.set-docker-ref.outputs.docker_ref }} + docker_build_tag: ${{ steps.set-docker-tag.outputs.docker_build_tag }} steps: - - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '${{ env.node }}' - cache: 'npm' - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '${{ env.java }}' - cache: 'gradle' - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 - - name: Production Build - run: ./gradlew -Pprod -Pwar clean bootWar - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: Artemis.war - path: build/libs/Artemis-*.war - - name: Upload Release Artifact - if: github.event_name == 'release' && github.event.action == 'created' - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: build/libs/Artemis-${{ github.event.release.tag_name }}.war - asset_name: Artemis.war - asset_content_type: application/x-webarchive + - name: Set Upload Release Artifact Outputs + id: set-upload-release + run: | + # If event is release created, set the release_upload flag and the release artifact details + if [[ "${{ github.event_name }}" == "release" && "${{ github.event.action }}" == "created" ]]; then + echo "release_upload=true" >> $GITHUB_OUTPUT + echo "release_url=${{ github.event.release.upload_url }}" >> $GITHUB_OUTPUT + echo "release_path=build/libs/Artemis-${{ github.event.release.tag_name }}.war" >> $GITHUB_OUTPUT + echo "release_name=Artemis.war" >> $GITHUB_OUTPUT + echo "release_type=application/x-webarchive" >> $GITHUB_OUTPUT + else + echo "release_upload=false" >> $GITHUB_OUTPUT + fi - docker: - name: Build and Push Docker Image - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'ls1intum/Artemis' }} - runs-on: ubuntu-latest - steps: - - name: Compute Tag - uses: actions/github-script@v7 - id: compute-tag - with: - result-encoding: string - script: | - if (context.eventName === "pull_request") { - return "pr-" + context.issue.number; - } - if (context.eventName === "release") { - return "latest"; - } - if (context.eventName === "push") { - if (context.ref.startsWith("refs/tags/")) { - return context.ref.slice(10); + - name: Set Docker Build Flag + id: set-docker-build + run: | + if [[ ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'ls1intum/Artemis' }} ]]; then + echo "docker_build=true" >> $GITHUB_OUTPUT + else + echo "docker_build=false" >> $GITHUB_OUTPUT + fi + + - name: Set Docker ref + if: ${{ steps.set-docker-build.outputs.docker_build == 'true' }} + id: set-docker-ref + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + # Checkout pull request HEAD commit instead of merge commit + # this is done to include the correct branch and git information inside the build + echo "docker_ref=${{ github.event.pull_request.head.ref }}" >> $GITHUB_OUTPUT + elif [[ "${{ github.event_name }}" == "push" ]]; then + echo "docker_ref=${{ github.ref_name }}" >> $GITHUB_OUTPUT + fi + + - name: Compute Docker Tag + if: ${{ steps.set-docker-build.outputs.docker_build == 'true' }} + uses: actions/github-script@v7 + id: compute-tag + with: + result-encoding: string + script: | + if (context.eventName === "pull_request") { + return "pr-" + context.issue.number; + } + if (context.eventName === "release") { + return "latest"; } - if (context.ref === "refs/heads/develop") { - return "develop"; + if (context.eventName === "push") { + if (context.ref.startsWith("refs/tags/")) { + return context.ref.slice(10); + } + if (context.ref === "refs/heads/develop") { + return "develop"; + } } - } - return "FALSE"; - - name: Git Checkout for PRs - if: ${{ github.event_name == 'pull_request' }} - # Checkout pull request HEAD commit instead of merge commit - # this is done to include the correct branch and git information inside the build - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - - name: Git Checkout for push actions - if: ${{ github.event_name == 'push' }} - uses: actions/checkout@v4 - with: - ref: ${{ github.ref_name }} - - name: Git Checkout for push actions - if: ${{ github.event_name == 'release' }} - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - # Build and Push to GitHub Container Registry - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - if: ${{ steps.compute-tag.outputs.result != 'FALSE' }} - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and Push to GitHub Container Registry - uses: docker/build-push-action@v5 - if: ${{ steps.compute-tag.outputs.result != 'FALSE' }} - with: - # beware that the linux/arm64 build from the registry is using an amd64 compiled .war file as - # the GitHub runners don't support arm64 and QEMU takes too long for emulating the build - platforms: linux/amd64,linux/arm64 - file: ./docker/artemis/Dockerfile - context: . - tags: ghcr.io/ls1intum/artemis:${{ steps.compute-tag.outputs.result }} - push: true - cache-from: type=gha - cache-to: type=gha,mode=min + return "FALSE"; + + - name: Set Docker Tag + id: set-docker-tag + run: | + if [[ ${{ steps.compute-tag.outputs.result != 'FALSE' }} ]]; then + echo "docker_build_tag=${{ steps.compute-tag.outputs.result }}" >> $GITHUB_OUTPUT + fi - # TODO: Push to Docker Hub (develop + tag) - # TODO: Push to Chair Harbour (??) + call-build-workflow: + name: Call Build Workflow + needs: define-inputs + uses: ./.github/workflows/reusable-build.yml + with: + build_war: true + release_upload: ${{ needs.define-inputs.outputs.release_upload == 'true' }} + release_url: ${{ needs.define-inputs.outputs.release_url }} + release_path: ${{ needs.define-inputs.outputs.release_path }} + release_name: ${{ needs.define-inputs.outputs.release_name }} + release_type: ${{ needs.define-inputs.outputs.release_type }} + docker: ${{ needs.define-inputs.outputs.docker_build == 'true' }} + docker_ref: ${{ needs.define-inputs.outputs.docker_ref }} + docker_build_tag: ${{ needs.define-inputs.outputs.docker_build_tag }} diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml new file mode 100644 index 000000000000..6960bf6c5b45 --- /dev/null +++ b/.github/workflows/reusable-build.yml @@ -0,0 +1,193 @@ +name: Build + +on: + workflow_call: + inputs: + # Build job inputs + build_war: + description: "Whether to build and upload the .war artifact." + required: false + default: false + type: boolean + build_ref: + description: "Branch name, tag, or commit SHA to use for the build job. If not provided, it falls back to the default behavior of actions/checkout." + required: false + default: '' + type: string + + # Upload Release Artifact job inputs + release_upload: + description: "Whether to upload the release artifact." + required: false + default: false + type: boolean + release_url: + description: "URL to upload the release artifact to." + required: false + default: '' + type: string + release_path: + description: "Path to the release artifact." + required: false + default: '' + type: string + release_name: + description: "Name of the release artifact." + required: false + default: '' + type: string + release_type: + description: "Content type of the release artifact." + required: false + default: '' + type: string + + # Docker job inputs + docker: + description: "Whether to build and push a Docker image." + required: false + default: false + type: boolean + docker_ref: + description: "Branch name, tag, or commit SHA to use for the Docker job. If not provided, it falls back to the default behavior of actions/checkout." + required: false + default: '' + type: string + docker_build_tag: + description: "Tag to use when building Docker image." + required: false + default: '' + type: string + +# Keep in sync with codeql-analysis.yml and test.yml and analysis-of-endpoint-connections.yml +env: + CI: true + node: 22 + java: 21 + +jobs: + validate-inputs: + name: Validate Inputs + runs-on: ubuntu-latest + steps: + - name: Validate Inputs + run: | + # Check release related inputs + if [[ "${{ github.event.inputs.release_upload }}" ]]; then + # List of required release inputs + missing_inputs=() + + # Check each required input + [[ -z "${{ inputs.release_url }}" || "${{ inputs.release_url }}" == '' ]] && missing_inputs+=("release_url") + [[ -z "${{ inputs.release_path }}" || "${{ inputs.release_path }}" == '' ]] && missing_inputs+=("release_path") + [[ -z "${{ inputs.release_name }}" || "${{ inputs.release_name }}" == '' ]] && missing_inputs+=("release_name") + [[ -z "${{ inputs.release_type }}" || "${{ inputs.release_type }}" == '' ]] && missing_inputs+=("release_type") + + if [[ "${#missing_inputs[@]}" -gt 0 ]]; then + echo "::error::Release upload is set to true, but the following inputs are missing: ${missing_inputs[*]}" + exit 1 + fi + fi + + # Check Docker related inputs + if [[ "${{ github.event.inputs.docker }}" ]]; then + # Check whether all Docker inputs are set + if [[ "${{ github.event.inputs.docker_build_tag }}" == '' ]]; then + echo "::error::Docker build is set to true, but Docker build tag is not set." + exit 1 + fi + fi + + + build: + name: Build .war artifact + if: ${{ inputs.build_war }} + needs: validate-inputs + runs-on: ubuntu-latest + steps: + # Git Checkout + - name: Git Checkout to the specific ref (if build_ref is set) + uses: actions/checkout@v4 + if: ${{ inputs.build_ref != '' }} + with: + ref: ${{ inputs.build_ref }} + - name: Git Checkout (default) + uses: actions/checkout@v4 + if: ${{ inputs.build_ref == '' }} + # Setup Node.js, Java and Gradle + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '${{ env.node }}' + cache: 'npm' + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '${{ env.java }}' + cache: 'gradle' + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + # Build + - name: Production Build + run: ./gradlew -Pprod -Pwar clean bootWar + # Upload Artifact + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: Artemis.war + path: build/libs/Artemis-*.war + # Upload Artifact (Release) + - name: Upload Release Artifact + if: ${{ inputs.release_upload }} + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ inputs.release_url }} + asset_path: ${{ inputs.release_path }} + asset_name: ${{ inputs.release_name }} + asset_content_type: ${{ inputs.release_type }} + + docker: + name: Build and Push Docker Image + if: ${{ inputs.docker }} + needs: validate-inputs + runs-on: ubuntu-latest + steps: + # Git Checkout + - name: Git Checkout to the specific ref (if docker_ref is set) + uses: actions/checkout@v4 + if: ${{ inputs.docker_ref != '' }} + with: + ref: ${{ inputs.docker_ref }} + - name: Git Checkout (default) + uses: actions/checkout@v4 + if: ${{ inputs.docker_ref == '' }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + # Build and Push to GitHub Container Registry + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and Push to GitHub Container Registry + uses: docker/build-push-action@v5 + with: + # beware that the linux/arm64 build from the registry is using an amd64 compiled .war file as + # the GitHub runners don't support arm64 and QEMU takes too long for emulating the build + platforms: linux/amd64,linux/arm64 + file: ./docker/artemis/Dockerfile + context: . + tags: ghcr.io/ls1intum/artemis:${{ inputs.docker_build_tag }} + push: true + cache-from: type=gha + cache-to: type=gha,mode=min + + # TODO: Push to Docker Hub (develop + tag) + + # TODO: Push to Chair Harbour (??) diff --git a/.github/workflows/testserver-deployment.yml b/.github/workflows/testserver-deployment.yml new file mode 100644 index 000000000000..5a36abd9f66f --- /dev/null +++ b/.github/workflows/testserver-deployment.yml @@ -0,0 +1,165 @@ +name: Deploy to a test-server + +on: + workflow_dispatch: + inputs: + branch_name: + description: "Which branch to deploy" + required: true + type: string + environment_name: + description: "Which environment to deploy (e.g. artemis-test7.artemis.cit.tum.de, etc.)." + required: true + type: string + triggered_by: + description: "Username that triggered deployment (not required, shown if triggered via GitHub UI, logged if triggered via GitHub app)" + required: false + type: string + + +concurrency: ${{ github.event.inputs.environment_name }} + +env: + CI: true + # Keep filename in sync with the workflow responsible for automatic builds on PRs + PR_AUTO_BUILD_FILE_NAME: "build.yml" + RAW_URL: https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.inputs.branch_name }} + +jobs: + # Log the inputs for debugging + log-inputs: + name: Log Inputs + runs-on: ubuntu-latest + steps: + - name: Print Inputs + run: | + echo "Branch: ${{ github.event.inputs.branch_name }}" + echo "Environment: ${{ github.event.inputs.environment_name }}" + echo "Triggered by: ${{ github.event.inputs.triggered_by }}" + echo "RAW_URL: ${{ env.RAW_URL }}" + + determine-build-context: + name: Determine Build Context + runs-on: ubuntu-latest + needs: log-inputs + outputs: + pr_number: ${{ steps.get_pr.outputs.pr_number }} + pr_head_sha: ${{ steps.get_pr.outputs.pr_head_sha }} + tag: ${{ steps.get_pr.outputs.tag }} + steps: + - name: Check if a PR exists for the branch + id: get_pr + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH_NAME=${{ github.event.inputs.branch_name }} + echo "Checking if PR exists for branch: $BRANCH_NAME targeting 'develop'." + + PR_DETAILS=$(gh api repos/${{ github.repository }}/pulls \ + --paginate \ + --jq ".[] | select(.head.ref == \"$BRANCH_NAME\" and .base.ref == \"develop\") | {number: .number, sha: .head.sha}") + + PR_NUMBER=$(echo "$PR_DETAILS" | jq -r ".number") + PR_HEAD_SHA=$(echo "$PR_DETAILS" | jq -r ".sha") + + if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then + echo "Found PR: $PR_NUMBER from branch: $BRANCH_NAME targeting 'develop' with Head: $PR_HEAD_SHA." + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "pr_head_sha=$PR_HEAD_SHA" >> $GITHUB_OUTPUT + echo "tag=pr-$PR_NUMBER" >> $GITHUB_OUTPUT + else + echo "No PR found for branch: $BRANCH_NAME targeting 'develop'." + echo "pr_number=" >> $GITHUB_OUTPUT + echo "pr_head_sha=" >> $GITHUB_OUTPUT + + # Fetch the latest commit SHA of the branch + LATEST_SHA=$(gh api repos/${{ github.repository }}/git/refs/heads/$BRANCH_NAME --jq '.object.sha') + + if [ -z "$LATEST_SHA" ]; then + echo "::error::Could not find the latest commit SHA for branch $BRANCH_NAME." + exit 1 + fi + + echo "Latest SHA for branch $BRANCH_NAME is $LATEST_SHA." + # Set tag as branch-SHA + echo "tag=branch-$LATEST_SHA" >> $GITHUB_OUTPUT + fi + + + # Build the Docker image (branch without PR) + conditional-build: + if: ${{ needs.determine-build-context.outputs.pr_number == '' }} + needs: determine-build-context + uses: ./.github/workflows/reusable-build.yml + with: + docker: true + docker_ref: ${{ github.event.inputs.branch_name }} + docker_build_tag: ${{ needs.determine-build-context.outputs.tag }} + + # Check if the build has run successfully (PR) + check-existing-build: + name: Check Existing Build + if: ${{ needs.determine-build-context.outputs.pr_number != '' }} + needs: determine-build-context + runs-on: ubuntu-latest + steps: + - name: Get latest successful build for branch + id: check_build + uses: octokit/request-action@v2.x + with: + route: GET /repos/${{ github.repository }}/actions/workflows/build.yml/runs?event=pull_request&status=success&head_sha=${{ needs.determine-build-context.outputs.pr_head_sha }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Fail if no successful build found + if: ${{ steps.check_build.conclusion == 'success' && fromJSON(steps.check_build.outputs.data).total_count == 0 }} + run: | + echo "::error::No successful build found for branch '${{ github.event.inputs.branch_name }}' with SHA '${{ needs.determine-build-context.outputs.pr_head_sha }}'." + exit 1 + + # Deploy to the test-server + deploy: + needs: [ determine-build-context, conditional-build, check-existing-build ] + # Run if either the conditional-build or check-existing-build job was successful + # Use always() since one of the jobs will always skip + if: always() && (needs.conditional-build.result == 'success' || needs.check-existing-build.result == 'success') + name: Deploy to Test-Server + runs-on: ubuntu-latest + environment: + name: ${{ github.event.inputs.environment_name }} + url: ${{ vars.DEPLOYMENT_URL }} + + env: + GATEWAY_USER: "jump" + GATEWAY_HOST: "gateway.artemis.in.tum.de:2010" + GATEWAY_HOST_PUBLIC_KEY: "[gateway.artemis.in.tum.de]:2010 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKtTLiKRILjKZ+Qg4ReWKsG7mLDXkzHfeY5nalSQUNQ4" + + steps: + # Download artemis-server-cli from GH without cloning the Repo + - name: Fetch Artemis CLI + run: | + wget ${{ env.RAW_URL }}/artemis-server-cli + chmod +x artemis-server-cli + + # Configure SSH Key + - name: Setup SSH Keys and known_hosts + env: + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + GATEWAY_SSH_KEY: "${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }}" + DEPLOYMENT_SSH_KEY: "${{ secrets.DEPLOYMENT_SSH_KEY }}" + run: | + mkdir -p ~/.ssh + ssh-agent -a $SSH_AUTH_SOCK > /dev/null + ssh-add - <<< $GATEWAY_SSH_KEY + ssh-add - <<< $DEPLOYMENT_SSH_KEY + cat - <<< $GATEWAY_HOST_PUBLIC_KEY >> ~/.ssh/known_hosts + + - name: Deploy Artemis with Docker + env: + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + DEPLOYMENT_USER: ${{ vars.DEPLOYMENT_USER }} + DEPLOYMENT_HOSTS: ${{ vars.DEPLOYMENT_HOSTS }} + TAG: ${{ needs.determine-build-context.outputs.tag }} + BRANCH_NAME: ${{ github.event.inputs.branch_name }} + DEPLOYMENT_FOLDER: ${{ vars.DEPLOYMENT_FOLDER }} + run: | + ./artemis-server-cli docker-deploy "$DEPLOYMENT_USER@$DEPLOYMENT_HOSTS" -g "$GATEWAY_USER@$GATEWAY_HOST" -t $TAG -b $BRANCH_NAME -d $DEPLOYMENT_FOLDER -y