diff --git a/.github/actions/gh_create_issue/action.yml b/.github/actions/gh_create_issue/action.yml new file mode 100644 index 000000000..4c960a2ba --- /dev/null +++ b/.github/actions/gh_create_issue/action.yml @@ -0,0 +1,64 @@ +name: Create a GitHub issue +description: "Create an issue on GitHub, and optionally add it to a project board." + +inputs: + title: + description: "The title of the issue." + required: true + owner: + description: "The owner of the repository to create the issue in." + required: false + default: ${{ github.repository_owner }} + repo: + description: "The repository to create the issue in." + required: false + default: ${{ github.repository }} + token: + description: "The GitHub token to use to authenticate." + required: false + default: ${{ github.token }} + body: + description: "The body of the issue." + required: false + body-file: + description: "The absolute path to a file containing the body of the issue." + required: false + assignee: + description: "The GitHub username to assign the issue to." + required: false + label: + description: "A comma-separated list of labels to add to the issue." + required: false + milestone: + description: "The milestone to add the issue to." + required: false + project: + description: "Number of the project to add the issue to." + required: false + template: + description: "The template to use for the issue." + required: false + fields: + description: "A JSON object containing the fields to use for the issue." + required: false + +outputs: + issue-url: + description: "The URL of the created issue." + value: ${{ steps.run.outputs.issue-url }} + +runs: + using: "composite" + steps: + - name: Run create_issue.sh + id: run + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + run: | + set -x + cat << EOF | tee inputs.json + ${{ toJSON(inputs) }} + EOF + out=$(./.github/actions/gh_create_issue/create_issue.sh inputs.json) + echo "issue-url=${out}" | tee -a "$GITHUB_OUTPUT" diff --git a/.github/actions/gh_create_issue/create_issue.sh b/.github/actions/gh_create_issue/create_issue.sh new file mode 100755 index 000000000..7d282c41d --- /dev/null +++ b/.github/actions/gh_create_issue/create_issue.sh @@ -0,0 +1,248 @@ +#!/usr/bin/env bash + +set -euo pipefail + +function debug() { + echo "DEBUG: $*" >&2 +} + +function warn() { + echo "WARN: $*" >&2 +} + +function inputs() { + name="${1}" + local val + val=$(jq -r ".\"${name}\"" "${inputFile}") + if [[ ${val} == "null" ]]; then + warn "Input ${name} not found in ${inputFile}" + return + fi + echo "${val}" +} + +function flagsFromInput() { + flagNames=("${@}") + for name in "${flagNames[@]}"; do + val=$(inputs "${name}") + if [[ -n ${val} ]]; then + echo "--${name}=${val}" + fi + done +} + +function createIssue() { + flags=( + "assignee" + "body" + "body-file" + "label" + "milestone" + "project" + "template" + "title" + ) + readarray -t flags <<< "$(flagsFromInput "${flags[@]}")" + flags+=("--repo=$(inputs owner)/$(inputs repo)") + debug gh issue create "${flags[@]}" + gh issue create "${flags[@]}" +} + +function listProjects() { + flags=( + "owner" + ) + readarray -t flags <<< "$(flagsFromInput "${flags[@]}")" + flags+=("--format=json") + debug gh project list "${flags[@]}" + gh project list "${flags[@]}" >> projects.json +} + +function findProjectID() { + project=$(inputs "project") + out="$( + jq -r \ + --arg project "${project}" \ + '.projects[] + | select(.title == $project) + | .id' \ + projects.json + )" + debug "Project ID: ${out}" + echo "${out}" +} + +function findProjectNo() { + project=$(inputs "project") + out="$( + jq -r \ + --arg project "${project}" \ + '.projects[] + | select(.title == $project) + | .number' \ + projects.json + )" + debug "Project Number: ${out}" + echo "${out}" +} + +function listItems() { + local projectNo="${1}" + flags=( + "owner" + ) + readarray -t flags <<< "$(flagsFromInput "${flags[@]}")" + flags+=("--limit=1000") + flags+=("--format=json") + debug gh project item-list "${flags[@]}" "${projectNo}" + gh project item-list "${flags[@]}" "${projectNo}" >> issues.json +} + +function findIssueItemID() { + local issueURL="${1}" + out="$( + jq -r \ + --arg issueURL "${issueURL}" \ + '.items[] + | select(.content.url == $issueURL) + | .id' \ + issues.json + )" + debug "Issue Item ID: ${out}" + echo "${out}" +} + +function listFields() { + local projectNo="${1}" + flags=( + "owner" + ) + readarray -t flags <<< "$(flagsFromInput "${flags[@]}")" + flags+=("--limit=1000") + flags+=("--format=json") + debug gh project field-list "${flags[@]}" "${projectNo}" + gh project field-list "${flags[@]}" "${projectNo}" >> fields.json +} + +function findFieldID() { + local fieldName="${1}" + out="$( + jq -r \ + --arg fieldName "${fieldName}" \ + '.fields[] + | select(.name == $fieldName) + | .id' \ + fields.json + )" + debug "Field ID of '${fieldName}': ${out}" + echo "${out}" +} + +function findSelectFieldID() { + local fieldName="${1}" + local fieldValue="${2}" + out="$( + jq -r \ + --arg fieldName "${fieldName}" \ + --arg fieldValue "${fieldValue}" \ + '.fields[] + | select(.name == $fieldName) + | .options[] + | select(.name == $fieldValue) + | .id' \ + fields.json + )" + debug "Field ID of '${fieldName}': ${out}" + echo "${out}" +} + +function findFieldType() { + local fieldName="${1}" + out="$( + jq -r \ + --arg fieldName "${fieldName}" \ + '.fields[] + | select(.name == $fieldName) + | .type' \ + fields.json + )" + debug "Field type of '${fieldName}': ${out}" + echo "${out}" +} + +function editItem() { + local projectID="${1}" + local itemID="${2}" + local id="${3}" + local value="${4}" + flags=( + "--project-id=${projectID}" + "--id=${itemID}" + "--field-id=${id}" + "--text=${value}" + ) + debug gh project item-edit "${flags[@]}" + gh project item-edit "${flags[@]}" > /dev/null +} + +function setFields() { + local projectID="${1}" + local itemID="${2}" + + fieldsLen="$(jq -r '.fields' "${inputFile}" | yq 'length')" + debug "Number of fields in input: ${fieldsLen}" + for ((i = 0; i < fieldsLen; i++)); do + name="$(jq -r '.fields' "${inputFile}" | + yq "to_entries | .[${i}].key")" + value="$(jq -r '.fields' "${inputFile}" | + yq "to_entries | .[${i}].value")" + debug "Field ${i}: ${name} = ${value}" + type=$(findFieldType "${name}") + + case "${type}" in + "ProjectV2Field") + id=$(findFieldID "${name}") + ;; + "ProjectV2SingleSelectField") + id=$(findSelectFieldID "${name}" "${value}") + ;; + *) + warn "Unknown field type: ${type}" + return 1 + ;; + esac + + editItem "${projectID}" "${itemID}" "${id}" "${value}" + done +} + +function main() { + inputFile="$(realpath "${1}")" + + workdir=$(mktemp -d) + pushd "${workdir}" > /dev/null + trap 'debug "not cleaning up, working directory at: ${workdir}"' ERR + + issueURL=$(createIssue) + echo "${issueURL}" + + project=$(inputs "project") + if [[ -z ${project} ]]; then + return + fi + + listProjects + projectNo=$(findProjectNo) + projectID=$(findProjectID) + + listItems "${projectNo}" + issueItemID=$(findIssueItemID "${issueURL}") + listFields "${projectNo}" + + setFields "${projectID}" "${issueItemID}" + + popd > /dev/null + rm -rf "${workdir}" +} + +main "${@}" diff --git a/.github/actions/notify_e2e_failure/action.yml b/.github/actions/notify_e2e_failure/action.yml index 75ce479b6..bb545e168 100644 --- a/.github/actions/notify_e2e_failure/action.yml +++ b/.github/actions/notify_e2e_failure/action.yml @@ -31,6 +31,52 @@ runs: id: pick-assignee uses: ./.github/actions/pick_assignee + - name: Get the current date + id: date + shell: bash + run: echo "CURRENT_DATE=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_ENV + + - name: Create body template + id: body-template + run: | + # TODO(katexochen): add job number when possible + jobURL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + # TODO(msanft): Add Self-managed param once logcollection is fixed. + opensearchURL="https://search-e2e-logs-y46renozy42lcojbvrt3qq7csm.eu-central-1.es.amazonaws.com/_dashboards/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-7d,to:now))&_a=(columns:!(metadata.name,systemd.unit,kubernetes.pod_name,message),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'74517cf0-6442-11ed-acf1-47dda8fdfbbb',key:metadata.github.e2e-test-provider,negate:!f,params:(query:${{ inputs.provider }}),type:phrase),query:(match_phrase:(metadata.github.e2e-test-provider:${{ inputs.provider }}))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'74517cf0-6442-11ed-acf1-47dda8fdfbbb',key:metadata.github.run-id,negate:!f,params:(query:${{ github.run_id }}),type:phrase),query:(match_phrase:(metadata.github.run-id:${{ github.run_id }}))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'74517cf0-6442-11ed-acf1-47dda8fdfbbb',key:metadata.github.ref-stream.keyword,negate:!f,params:(query:'${{ inputs.refStream }}'),type:phrase),query:(match_phrase:(metadata.github.ref-stream.keyword:'${{ inputs.refStream }}'))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'74517cf0-6442-11ed-acf1-47dda8fdfbbb',key:metadata.github.kubernetes-version.keyword,negate:!f,params:(query:'${{ inputs.kubernetesVersion }}'),type:phrase),query:(match_phrase:(metadata.github.kubernetes-version.keyword:'${{ inputs.kubernetesVersion }}'))),('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'74517cf0-6442-11ed-acf1-47dda8fdfbbb',key:metadata.github.e2e-test-payload,negate:!f,params:(query:'${{ inputs.test }}'),type:phrase),query:(match_phrase:(metadata.github.e2e-test-payload:'${{ inputs.test }}')))),index:'74517cf0-6442-11ed-acf1-47dda8fdfbbb',interval:auto,query:(language:kuery,query:''),sort:!())" + cat << EOF > header.md + + ## Metadata + + * [Job URL](${jobURL}) + * [OpenSearch URL](${opensearchURL// /%20}) + + EOF + + cat header.md .github/failure_project_template.md > body.md + echo "BODY_PATH=$(pwd)/body.md" >> $GITHUB_ENV + + - uses: ./.github/actions/gh_create_issue + id: gh_create_issue + with: + title: "${{ env.CURRENT_DATE }}" + body-file: ${{ env.BODY_PATH }} + repo: issues + label: "e2e failure" + assignee: ${{ steps.pick-assignee.outputs.assignee }} + project: Constellation bugs + fields: | + Status: New failures + workflow: ${{ github.workflow }} + kubernetesVersion: ${{ inputs.kubernetesVersion }} + cloudProvider: ${{ inputs.provider }} + test: ${{ inputs.test }} + refStream: ${{ inputs.refStream }} + token: ${{ inputs.projectWriteToken }} + + - name: Issue URL ${{ steps.gh_create_issue.outputs.issue-url }} + shell: bash + run: echo ${{ steps.gh_create_issue.outputs.issue-url }} + - name: Create project card in case of failure id: create-project-card continue-on-error: true @@ -112,13 +158,3 @@ runs: echo "additionalFields=$(cat facts.json)" | tee -a "$GITHUB_OUTPUT" echo "additionalButtons=$buttons" | tee -a "$GITHUB_OUTPUT" - - - name: Notify teams channel - continue-on-error: true - uses: ./.github/actions/notify_teams - with: - teamsWebhookURI: ${{ inputs.teamsWebhookUri }} - title: "Constellation E2E test failed" - assignee: ${{ steps.pick-assignee.outputs.assignee }} - additionalFields: ${{ steps.create-fields.outputs.additionalFields }} - additionalButtons: ${{ steps.create-fields.outputs.additionalButtons }}