diff --git a/bake-apm-service-version/action.sh b/bake-apm-service-version/action.sh
new file mode 100755
index 0000000..c6c3be7
--- /dev/null
+++ b/bake-apm-service-version/action.sh
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Resolve project ID: prefer input, else gcloud config
+PROJECT_ID_RESOLVED="${PROJECT_ID:-}"
+if [[ -z "$PROJECT_ID_RESOLVED" ]]; then
+ PROJECT_ID_RESOLVED="$(gcloud config get-value project 2>/dev/null || true)"
+fi
+
+if [[ -z "$PROJECT_ID_RESOLVED" || "$PROJECT_ID_RESOLVED" == "(unset)" ]]; then
+ echo "Could not determine GCP project from gcloud config. Ensure auth step set the project, or pass project_id input."
+ exit 1
+fi
+
+if [[ -z "${GCP_REGION:-}" ]]; then
+ echo "Input gcp_region is required"
+ exit 1
+fi
+
+if [[ -z "${REPOSITORY_NAME:-}" ]]; then
+ echo "Input repository_name is required"
+ exit 1
+fi
+
+if [[ -z "${TAG_VERSION:-}" ]]; then
+ echo "Input tag_version is required"
+ exit 1
+fi
+
+IMAGE_URL="${GCP_REGION}-docker.pkg.dev/${PROJECT_ID_RESOLVED}/${PROJECT_ID_RESOLVED}/${REPOSITORY_NAME}:${GITHUB_SHA}"
+TMPDIR="$(mktemp -d)"
+trap 'rm -rf "$TMPDIR"' EXIT
+
+echo "Baking ELASTIC_APM_SERVICE_VERSION into image: $IMAGE_URL"
+
+# Build a tiny wrapper image setting ELASTIC_APM_SERVICE_VERSION, using the resolved base image
+cat > "$TMPDIR/Dockerfile" << EOF
+FROM ${IMAGE_URL}
+ENV ELASTIC_APM_SERVICE_VERSION=${TAG_VERSION}
+EOF
+
+# Build and push to the same tag so Terraform deploys this exact image
+gcloud builds submit --tag "$IMAGE_URL" "$TMPDIR"
+
+echo "Successfully built and pushed: $IMAGE_URL"
+
diff --git a/bake-apm-service-version/action.yml b/bake-apm-service-version/action.yml
new file mode 100644
index 0000000..02c6d3c
--- /dev/null
+++ b/bake-apm-service-version/action.yml
@@ -0,0 +1,29 @@
+name: Bake APM Service Version
+description: Build and push an image that sets ELASTIC_APM_SERVICE_VERSION using Cloud Build
+
+inputs:
+ gcp_region:
+ required: true
+ description: GCP region, e.g., us-central1
+ repository_name:
+ required: true
+ description: Artifact Registry repository name
+ tag_version:
+ required: true
+ description: Value for ELASTIC_APM_SERVICE_VERSION
+ project_id:
+ required: false
+ description: Optional GCP project ID (overrides gcloud config)
+
+runs:
+ using: composite
+ steps:
+ - name: Bake ELASTIC_APM_SERVICE_VERSION into image tag
+ shell: bash
+ run: ${{ github.action_path }}/action.sh
+ env:
+ GCP_REGION: ${{ inputs.gcp_region }}
+ REPOSITORY_NAME: ${{ inputs.repository_name }}
+ TAG_VERSION: ${{ inputs.tag_version }}
+ PROJECT_ID: ${{ inputs.project_id }}
+
diff --git a/environment-dotnet/action.sh b/environment-dotnet/action.sh
new file mode 100755
index 0000000..414919e
--- /dev/null
+++ b/environment-dotnet/action.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+if [ "$GITHUB_REF_NAME" = 'main' ]; then
+ ENVIRONMENT='prd'
+ JWT_JWKS_PATH="https://api.dotz.com.br/accounts/api/default/"
+elif [ "$GITHUB_REF_NAME" = 'staging' ]; then
+ ENVIRONMENT='uat'
+ JWT_JWKS_PATH="https://uat.dotznext.com/accounts/api/default/"
+elif [ "$GITHUB_REF_NAME" = 'qa' ]; then
+ ENVIRONMENT='uat'
+ JWT_JWKS_PATH="https://uat.dotznext.com/accounts/api/default/"
+else
+ ENVIRONMENT='dev'
+ JWT_JWKS_PATH="https://uat.dotznext.com/accounts/api/default/"
+fi
+
+{
+ echo ""
+ if ! [ "$ENVIRONMENT" = "dev" ]; then
+ echo "ELASTIC_APM_ENABLED: 'true'"
+ fi
+ echo "JWT_JWKS_PATH: '$JWT_JWKS_PATH'"
+ echo "JKS_SAFE_IPS: '$JKS_SAFE_IPS'"
+ echo "JKS_GOOGLE_PROJECTID: '$GOOGLE_PROJECTID'"
+ echo "ELASTIC_APM_SERVER_URL: '$ELASTIC_APM_SERVER_URL'"
+} > tmpfile
+
+cat ./envs/env-$ENVIRONMENT.yaml tmpfile > merged_temp.yaml
+awk '
+ BEGIN { FS=": " }
+ !/^[[:space:]]*$/ && !/^#/ {
+ key = $1
+ if (!seen[key]) {
+ print
+ seen[key] = 1
+ }
+ }
+' merged_temp.yaml > ./envs/env-$ENVIRONMENT.yaml
+rm tmpfile merged_temp.yaml
diff --git a/environment-dotnet/action.yml b/environment-dotnet/action.yml
new file mode 100644
index 0000000..0f01772
--- /dev/null
+++ b/environment-dotnet/action.yml
@@ -0,0 +1,23 @@
+name: Create Env
+description: Create Env
+
+inputs:
+ ref_name:
+ required: true
+ description: ref_name
+ project_id:
+ required: true
+ description: project_id
+ safe_ips:
+ required: true
+ description: safe_ips
+runs:
+ using: composite
+ steps:
+ - name: Create Env
+ shell: bash
+ run: ${{ github.action_path }}/action.sh
+ env:
+ GITHUB_REF_NAME: ${{ inputs.ref_name }}
+ GOOGLE_PROJECTID: ${{ inputs.project_id}}
+ JKS_SAFE_IPS: ${{ inputs.safe_ips }}
diff --git a/environment-test-dotnet/action.sh b/environment-test-dotnet/action.sh
new file mode 100755
index 0000000..596fb36
--- /dev/null
+++ b/environment-test-dotnet/action.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+FILE=./envs/env-dev.yaml
+while IFS= read -r line
+do
+ if [[ ! $line =~ ^# ]] && [[ $line =~ [^[:space:]] ]]
+ then
+ IFS=":" read -r key value <<< "$line"
+ key=$(echo "$key" | xargs)
+ value=$(echo "$value" | xargs)
+ echo "$key=$value" >> "$GITHUB_ENV"
+ fi
+done < "$FILE"
diff --git a/environment-test-dotnet/action.yml b/environment-test-dotnet/action.yml
new file mode 100644
index 0000000..79270da
--- /dev/null
+++ b/environment-test-dotnet/action.yml
@@ -0,0 +1,9 @@
+name: Create Env
+description: Create Env
+
+runs:
+ using: composite
+ steps:
+ - name: Create Env Test
+ shell: bash
+ run: ${{ github.action_path }}/action.sh
diff --git a/gmud-creator/action.yml b/gmud-creator/action.yml
new file mode 100644
index 0000000..389aed8
--- /dev/null
+++ b/gmud-creator/action.yml
@@ -0,0 +1,362 @@
+name: "Gmud Creator Action"
+description: "Create a Gmud on GLPI"
+
+inputs:
+ glpi_api_url:
+ description: "The base URL of the GLPI API"
+ required: true
+ glpi_app_token:
+ description: "The application token for GLPI"
+ required: true
+ glpi_user_token:
+ description: "The user token for GLPI"
+ required: true
+ slack_bot_user_oauth_access_token:
+ description: "Slack Bot User OAuth Access Token for sending notifications"
+ required: true
+ slack_channel:
+ description: "Slack channel to send notifications"
+ required: true
+
+runs:
+ using: "composite"
+ steps:
+ - name: Validate GLPI_API_URL format
+ shell: bash
+ run: |
+ if [[ ! "$GLPI_API_URL" =~ ^https?:// ]]; then
+ echo "Error: GLPI_API_URL does not start with http:// or https://"
+ exit 1
+ fi
+ echo "GLPI_API_URL format is valid"
+ env:
+ GLPI_API_URL: ${{ inputs.glpi_api_url }}
+
+ - name: Extract PR Data
+ id: extract-data
+ uses: actions/github-script@v6
+ with:
+ script: |
+ const prBody = context.payload.pull_request.body;
+ const extractSection = (title) => {
+ const regex = new RegExp(`### ${title}[\\s\\S]*?\\n([\\s\\S]*?)(?=###|$)`);
+ const match = prBody.match(regex);
+ return match
+ ? match[1]
+ .trim()
+ .replace(/^- \[.\]|\*|^[\[\]]|[\[\]]$|\[|\]/g, '')
+ .trim()
+ : '';
+ };
+
+ function extractResponsible(field) {
+ const regex = new RegExp(`\\*\\*${field}:\\*\\*\\s*\\[(.*?)\\]`, 'i');
+ const match = prBody.match(regex);
+ return (match || [])[1] || 'Não especificado';
+ };
+
+ const changeType =
+ /-\s\[x\]\s\*\*Imediata\*\*/i.test(prBody) ? 'Imediata' :
+ /-\s\[x\]\s\*\*Não Programada\*\*/i.test(prBody) ? 'Não Programada' :
+ /-\s\[x\]\s\*\*Programada\*\*/i.test(prBody) ? 'Programada' : '';
+
+ const tested = /-\s\[x\]\s\*\*Confirmo\*\*/i.test(prBody) ? 'Sim' : 'Não';
+
+ const summary = extractSection('Descrição Resumida');
+ const contxt = extractSection('Contexto');
+ const problem = extractSection('Problema') || 'N/A';
+ const change = extractSection('Mudança Proposta');
+ const impact = extractSection('Descrição de impacto');
+ const task_link = extractSection('Link da Task');
+
+ const dev = extractResponsible('Dev');
+ const tl = extractResponsible('TL');
+ const team = extractResponsible('Team');
+ const approver = extractSection('Aprovador') || 'Não especificado';
+
+ // Output para os próximos passos
+ core.setOutput('change_type', changeType);
+ core.setOutput('summary', summary);
+ core.setOutput('context', contxt);
+ core.setOutput('problem', problem);
+ core.setOutput('change', change);
+ core.setOutput('dev', dev);
+ core.setOutput('tl', tl);
+ core.setOutput('team', team);
+ core.setOutput('approver', approver);
+ core.setOutput('impact', impact);
+ core.setOutput('task_link', task_link);
+ core.setOutput('pr_url', context.payload.pull_request.html_url);
+ core.setOutput('tested', tested);
+
+ - name: Get PR Approvers
+ id: get-approvers
+ uses: actions/github-script@v6
+ with:
+ script: |
+ const { owner, repo } = context.repo;
+ const pr_number = context.payload.pull_request.number;
+
+ const reviews = await github.rest.pulls.listReviews({
+ owner,
+ repo,
+ pull_number: pr_number
+ });
+
+ const approverMap = new Map();
+ reviews.data.forEach(review => {
+ if (review.state === 'APPROVED') {
+ approverMap.set(review.user.login, {
+ username: review.user.login,
+ avatar_url: review.user.avatar_url,
+ submitted_at: review.submitted_at
+ });
+ } else if (review.state === 'CHANGES_REQUESTED' || review.state === 'DISMISSED') {
+ approverMap.delete(review.user.login);
+ }
+ });
+
+ const approvers = Array.from(approverMap.values())
+ .sort((a, b) => new Date(a.submitted_at) - new Date(b.submitted_at));
+
+ let approversList = '';
+ if (approvers.length > 0) {
+ approversList = approvers.map(approver => `• @${approver.username}`).join('\\n');
+ } else {
+ approversList = '• _Nenhum approval encontrado_';
+ }
+
+ core.setOutput('approvers_list', approversList);
+ core.setOutput('approvers_count', approvers.length.toString());
+
+ - name: Create GMUD via API
+ shell: bash
+ env:
+ GLPI_API_URL: ${{ inputs.glpi_api_url }}
+ GLPI_APP_TOKEN: ${{ inputs.glpi_app_token }}
+ GLPI_USER_TOKEN: ${{ inputs.glpi_user_token }}
+ run: |
+ echo "Initializing GLPI session at \$GLPI_API_URL/initSession"
+ SESSION_RESPONSE=$(curl -s -w "%{http_code}" --max-time 10 -X GET \
+ -H "App-Token: $GLPI_APP_TOKEN" \
+ -H "Authorization: user_token $GLPI_USER_TOKEN" \
+ -H "Content-Type: application/json" \
+ "$GLPI_API_URL/initSession" -o session.json 2> session_error.log || echo "Curl failed with exit code $?")
+
+ SESSION_STATUS=${SESSION_RESPONSE##* }
+ echo "Session HTTP Status: $SESSION_STATUS"
+
+ if [ -s session_error.log ]; then
+ echo "Session init error details:"
+ cat session_error.log
+ fi
+
+ if [ ! -f session.json ]; then
+ echo "Error: session.json not created"
+ exit 1
+ fi
+
+ SESSION_TOKEN=$(jq -r '.session_token' session.json 2>/dev/null)
+ if [ -z "$SESSION_TOKEN" ]; then
+ echo "Error: Failed to extract session_token from session.json"
+ cat session.json
+ exit 1
+ fi
+
+ GMUD_CONTENT=$(cat <
${{ steps.extract-data.outputs.tested }}
+Contexto:
+${{ steps.extract-data.outputs.context }}
+Problema:
+${{ steps.extract-data.outputs.problem }}
+Mudança:
+${{ steps.extract-data.outputs.change }}
+PR(s):
+${{ steps.extract-data.outputs.pr_url }}
+Link da task no Azure:
+${{ steps.extract-data.outputs.task_link }}
+Responsáveis:
+Dev: ${{ steps.extract-data.outputs.dev }}
+TL: ${{ steps.extract-data.outputs.tl }}
+Team: ${{ steps.extract-data.outputs.team }}
+Aprovador:
+${{ steps.extract-data.outputs.approver }}
+ EOF + ) + + GMUD_JSON=$(jq -n --arg name "[${{ github.repository }}] - ${{ steps.extract-data.outputs.summary }}" \ + --arg content "$GMUD_CONTENT" \ + --arg dev "${{ steps.extract-data.outputs.dev }}" \ + --arg tl "${{ steps.extract-data.outputs.tl }}" \ + --arg impact "${{ steps.extract-data.outputs.impact }}" \ + '{ + "input": { + "name": $name, + "content": $content, + "type": "change", + "status": "1", + "impact": "3", + "urgency": "3", + "itilcategories_id": "1", + "devfield": $dev, + "qafield": $tl, + "rolloutplancontent": "Merge+do+PR+após+aprovação.
", + "impactcontent": $impact + } + }') + + echo "GMUD JSON payload:" + echo "$GMUD_JSON" | jq . 2>/dev/null || { + echo "Error: Invalid JSON payload" + echo "$GMUD_JSON" + exit 1 + } + + RESPONSE=$(curl -s -w "%{http_code}" -X POST \ + -H "App-Token: $GLPI_APP_TOKEN" \ + -H "Session-Token: $SESSION_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$GMUD_JSON" \ + "$GLPI_API_URL/Change" -o response.json 2> curl_error.log || echo "Curl failed with exit code $?") + + echo "HTTP Status: $RESPONSE - $GLPI_API_URL" + + if [ -s curl_error.log ]; then + echo "Curl error output:" + cat curl_error.log + fi + + if [ -f response.json ]; then + echo "Response content:" + cat response.json + else + echo "Error: response.json was not created" + exit 1 + fi + + if [ -n "$RESPONSE" ] && [ "$RESPONSE" -ge 400 ] 2>/dev/null; then + echo "Error: API request failed with status $RESPONSE" + exit 1 + elif [ -z "$RESPONSE" ]; then + echo "Error: No HTTP status code returned." + exit 1 + fi + + - name: Extract GLPI GMUD ID + id: glpi-id + shell: bash + run: | + if [ -f response.json ]; then + GMUD_ID=$(jq -r '.id' response.json) + echo "GMUD_ID=$GMUD_ID" >> $GITHUB_OUTPUT + echo "GLPI GMUD ID: $GMUD_ID" + else + echo "Error: response.json not found" + exit 1 + fi + + - name: Build Slack blocks payload + id: build-slack-blocks + shell: bash + run: | + jq -n \ + --arg repo "${{ github.repository }}" \ + --arg change_type "${{ steps.extract-data.outputs.change_type }}" \ + --arg summary "${{ steps.extract-data.outputs.summary }}" \ + --arg dev "${{ steps.extract-data.outputs.dev }}" \ + --arg tl "${{ steps.extract-data.outputs.tl }}" \ + --arg team "${{ steps.extract-data.outputs.team }}" \ + --arg approvers_count "${{ steps.get-approvers.outputs.approvers_count }}" \ + --arg approvers_list "${{ steps.get-approvers.outputs.approvers_list }}" \ + --arg gmud_id "${{ steps.glpi-id.outputs.GMUD_ID }}" \ + --arg pr_url "${{ steps.extract-data.outputs.pr_url }}" \ + '[ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "📢 Nova GMUD Criada 📢", + "emoji": true + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": ("*Repositório:*\n" + $repo) + }, + { + "type": "mrkdwn", + "text": ("*Tipo de Mudança:*\n" + $change_type) + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ("*Descrição:*\n" + $summary) + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": ("*Responsáveis:*\n• *Dev:* " + $dev + "\n• *TL:* " + $tl + "\n• *Team:* " + $team) + }, + { + "type": "mrkdwn", + "text": ("*Aprovadores (" + $approvers_count + "):*\n" + $approvers_list) + } + ] + }, + { + "type": "divider" + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Abrir no GLPI", + "emoji": true + }, + "url": ("https://suporte.dotz.com.br/front/change.form.php?id=" + $gmud_id), + "style": "primary" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Ver PR no GitHub", + "emoji": true + }, + "url": $pr_url + } + ] + } + ]' > slack_blocks.json + + BLOCKS=$(jq -c . slack_blocks.json) + BLOCKS=${BLOCKS//'%'/'%25'} + BLOCKS=${BLOCKS//$'\n'/'%0A'} + BLOCKS=${BLOCKS//$'\r'/'%0D'} + echo "blocks=$BLOCKS" >> "$GITHUB_OUTPUT" + + - name: Send GMUD payload notification + uses: archive/github-actions-slack@v2.9.0 + with: + slack-bot-user-oauth-access-token: ${{ inputs.slack_bot_user_oauth_access_token }} + slack-channel: ${{ inputs.slack_channel }} + slack-blocks: ${{ steps.build-slack-blocks.outputs.blocks }} diff --git a/rollback-environment-dotnet/action.sh b/rollback-environment-dotnet/action.sh new file mode 100755 index 0000000..5c3d704 --- /dev/null +++ b/rollback-environment-dotnet/action.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +JWT_JWKS_PATH="https://api.dotz.com.br/accounts/api/default/" + +{ + echo "" + echo "JWT_JWKS_PATH: '$JWT_JWKS_PATH'" + echo "JKS_SAFE_IPS: '$JKS_SAFE_IPS'" + echo "JKS_GOOGLE_PROJECTID: '$GOOGLE_PROJECTID'" + echo "ELASTIC_APM_SERVER_URL: '$ELASTIC_APM_SERVER_URL'" +} > tmpfile + +cat ./envs/env-prd.yaml tmpfile > merged_temp.yaml +awk ' + BEGIN { FS=": " } + !/^[[:space:]]*$/ && !/^#/ { + key = $1 + if (!seen[key]) { + print + seen[key] = 1 + } + } +' merged_temp.yaml > ./envs/env-prd.yaml +rm tmpfile merged_temp.yaml diff --git a/rollback-environment-dotnet/action.yml b/rollback-environment-dotnet/action.yml new file mode 100644 index 0000000..0f01772 --- /dev/null +++ b/rollback-environment-dotnet/action.yml @@ -0,0 +1,23 @@ +name: Create Env +description: Create Env + +inputs: + ref_name: + required: true + description: ref_name + project_id: + required: true + description: project_id + safe_ips: + required: true + description: safe_ips +runs: + using: composite + steps: + - name: Create Env + shell: bash + run: ${{ github.action_path }}/action.sh + env: + GITHUB_REF_NAME: ${{ inputs.ref_name }} + GOOGLE_PROJECTID: ${{ inputs.project_id}} + JKS_SAFE_IPS: ${{ inputs.safe_ips }} diff --git a/sonarqube-angular/action.yml b/sonarqube-angular/action.yml new file mode 100644 index 0000000..71fab6f --- /dev/null +++ b/sonarqube-angular/action.yml @@ -0,0 +1,103 @@ +name: 'Sonarqube Angular Scanner' +description: 'Quality Gate Sonarqube for Angular projects' + +inputs: + sonar-verbose: + required: false + type: boolean + default: false + sonar-sources: + required: false + type: string + default: "." + sonar-test-inclusions: + required: false + type: string + default: "**/*.spec.ts" + sonar-exclusions: + required: false + type: string + default: "**/*.html,**/*.css,**/*.scss,**/*.spec.ts,**/*.json,**/*.js" + sonar-source-encoding: + required: false + type: string + default: "UTF-8" + sonar-typescript-tsconfig-path: + required: false + type: string + default: "tsconfig.json" + sonar-report-paths: + required: false + type: string + default: "lcov.info" + sonar-host-url: + required: true + type: string + sonar-api-token: + required: true + type: string + sonar-token: + required: true + type: string + sonar-project-key: + required: true + type: string + sonar-timeout: + required: false + type: string + default: "300" + sonar-project-name: + required: true + type: string + node-version: + required: false + type: string + default: "20.x" + +runs: + using: composite + steps: + - name: SonarQube Scan + uses: sonarsource/sonarqube-scan-action@v5.2.0 + continue-on-error: true + with: + args: > + -Dsonar.projectKey=${{ inputs.sonar-project-key }} + -Dsonar.projectName="${{ inputs.sonar-project-key }}" + -Dsonar.sources="${{ inputs.sonar-sources }}" + -Dsonar.typescript.lcov.reportPaths=${{ inputs.sonar-report-paths }} + -Dsonar.exclusions=${{ inputs.sonar-exclusions }} + -Dsonar.verbose=${{ inputs.sonar-verbose }} + -Dsonar.sourceEncoding=${{ inputs.sonar-source-encoding }} + env: + SONAR_TOKEN: ${{ inputs.sonar-token }} + SONAR_HOST_URL: ${{ inputs.sonar-host-url }} + + - name: Check SonarQube Quality Gate Status + id: sonarqube-status + shell: bash + continue-on-error: true + run: ${{ github.action_path }}/sonarqube-status.sh + env: + SONAR_PROJECT_KEY: ${{ inputs.sonar-project-key }} + SONAR_TOKEN: ${{ inputs.sonar-api-token }} + SONAR_HOST_URL: ${{ inputs.sonar-host-url }} + + - name: Get Coverage + shell: bash + id: coverage-calc + if: steps.sonarqube-status.outputs.status == 'OK' + continue-on-error: true + run: ${{ github.action_path }}/coverage-calc.sh + + - name: Update Quality Gates Sonarqube + shell: bash + id: quality-gates-sonarqube + if: steps.sonarqube-status.outputs.status == 'OK' + continue-on-error: true + run: ${{ github.action_path }}/quality-gates-sonarqube.sh + env: + SONAR_PROJECT_KEY: ${{ inputs.sonar-project-key }} + SONAR_TOKEN: ${{ inputs.sonar-api-token }} + SONAR_HOST_URL: ${{ inputs.sonar-host-url }} + LEGACY_COV_NUM: ${{ steps.coverage-calc.outputs.coverage }} diff --git a/sonarqube-angular/coverage-calc.sh b/sonarqube-angular/coverage-calc.sh new file mode 100755 index 0000000..b94fc77 --- /dev/null +++ b/sonarqube-angular/coverage-calc.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +COVERAGE=$(python -c "import xml.etree.ElementTree as ET; \ +print(float(ET.parse('coverage.xml').getroot().attrib['line-rate']) * 100)") + +echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT +echo "legacy_coverage=$COVERAGE" >> $GITHUB_OUTPUT diff --git a/sonarqube-angular/quality-gates-sonarqube.sh b/sonarqube-angular/quality-gates-sonarqube.sh new file mode 100755 index 0000000..3cb5c5f --- /dev/null +++ b/sonarqube-angular/quality-gates-sonarqube.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +PROJECT_RESPONSE=$(curl -s -u "$SONAR_TOKEN:" \ +"$SONAR_HOST_URL/api/qualitygates/get_by_project?project=$SONAR_PROJECT_KEY") + +QUALITY_GATE_NAME=$(echo "$PROJECT_RESPONSE" | jq -r '.qualityGate.name') +QUALITY_GATE_NAME=$(echo "$QUALITY_GATE_NAME" | jq -sRr @uri) + +echo "Quality Gate Name: $QUALITY_GATE_NAME" + +GATE_RESPONSE=$(curl -s -u "$SONAR_TOKEN:" \ +"$SONAR_HOST_URL/api/qualitygates/show?name=$QUALITY_GATE_NAME") + +NEW_COV_CONDITION_ID=$(echo "$GATE_RESPONSE" | jq -r '.conditions[] | select(.metric=="coverage") | .id') +CURRENT_NEW_COV=$(echo "$GATE_RESPONSE" | jq -r '.conditions[] | select(.metric=="coverage") | .error') + +if [ -n "$NEW_COV_CONDITION_ID" ]; then + CURRENT_NEW_COV_NUM=$(echo "$CURRENT_NEW_COV" | sed 's/[^0-9.]*//g') + + if (( $(echo "$LEGACY_COV_NUM > $CURRENT_NEW_COV_NUM" | bc -l) )); then + if (( $(echo "$LEGACY_COV_NUM > 80" | bc -l) )); then + echo "Legacy coverage ($LEGACY_COV_NUM%) > 80%, updating coverage threshold to 80%" + curl -u "$SONAR_TOKEN:" -X POST \ + "$SONAR_HOST_URL/api/qualitygates/update_condition" \ + -d "id=$NEW_COV_CONDITION_ID&metric=coverage&op=LT&error=80" + else + echo "Updating coverage threshold from $CURRENT_NEW_COV to $LEGACY_COV_NUM%" + curl -u "$SONAR_TOKEN:" -X POST \ + "$SONAR_HOST_URL/api/qualitygates/update_condition" \ + -d "id=$NEW_COV_CONDITION_ID&metric=coverage&op=LT&error=$LEGACY_COV_NUM" + fi + else + echo "Legacy coverage ($LEGACY_COV_NUM%) not greater than current coverage ($CURRENT_NEW_COV), keeping existing configuration" + fi +else + echo "Criando nova condição para coverage" + curl -u "$SONAR_TOKEN:" -X POST \ + "$SONAR_HOST_URL/api/qualitygates/create_condition" \ + -d "gateName=$QUALITY_GATE_NAME&metric=coverage&op=LT&error=$LEGACY_COV_NUM" +fi diff --git a/sonarqube-angular/sonarqube-status.sh b/sonarqube-angular/sonarqube-status.sh new file mode 100755 index 0000000..7c2d912 --- /dev/null +++ b/sonarqube-angular/sonarqube-status.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +sleep 15 +SONAR_STATUS=$(curl -s -u "$SONAR_TOKEN:" \ +"$SONAR_HOST_URL/api/qualitygates/project_status?projectKey=$SONAR_PROJECT_KEY") + +# Extract overall status +STATUS=$(echo "$SONAR_STATUS" | jq -r '.projectStatus.status') + +# Process failing conditions +echo "SonarQube Quality Gate Status: $STATUS" +if [ "$STATUS" != "OK" ]; then + echo "=== Failing Conditions ===" + echo "$SONAR_STATUS" | jq -r '.projectStatus.conditions[] | select(.status != "OK") | "Metric: \(.metricKey) | Threshold: \(.errorThreshold) | Actual: \(.actualValue)"' + + echo "status=$STATUS" >> $GITHUB_OUTPUT + echo "::error::Quality Gate failed with status: $STATUS" + exit 1 +fi + +echo "status=$STATUS" >> $GITHUB_OUTPUT +echo "Quality Gate passed successfully!" diff --git a/sonarqube-dotnet/action.yml b/sonarqube-dotnet/action.yml new file mode 100644 index 0000000..5b898ab --- /dev/null +++ b/sonarqube-dotnet/action.yml @@ -0,0 +1,119 @@ +name: 'Sonarqube .NET Scanner' +description: 'Quality Gate Sonarqube for .NET projects' + +inputs: + sonar-verbose: + required: false + type: boolean + default: false + sonar-sources: + required: false + type: string + default: "." + sonar-report-paths: + required: false + type: string + default: "coverage.xml" + sonar-source-encoding: + required: false + type: string + default: "UTF-8" + sonar-exclusions: + required: false + type: string + default: "**/bin/**,**/obj/**,**/TestResults/**,**/Tests/**,**/*.py" + sonar-host-url: + required: true + type: string + sonar-api-token: + required: true + type: string + sonar-token: + required: true + type: string + sonar-project-key: + required: true + type: string + sonar-timeout: + required: false + type: string + default: "300" + sonar-dotnet-version: + required: false + type: string + default: "8.0.x" + sonar-project-name: + required: true + type: string + + +runs: + using: composite + steps: + - name: Setup SonarScanner + shell: bash + run: | + # Install or update without failing if already installed + { dotnet tool update --global dotnet-sonarscanner || \ + { [ $? -eq 0 ] || dotnet tool install --global dotnet-sonarscanner; }; } && \ + echo "$HOME/.dotnet/tools" >> $GITHUB_PATH + + - name: Verify SonarScanner + shell: bash + run: | + echo "SonarScanner path: $(which dotnet-sonarscanner)" + echo "Version: $(dotnet-sonarscanner --version)" + + - name: Start SonarQube Analysis + shell: bash + run: | + dotnet-sonarscanner begin \ + /k:"${{ inputs.sonar-project-key }}" \ + /n:"${{ inputs.sonar-project-name }}" \ + /d:sonar.host.url="${{ inputs.sonar-host-url }}" \ + /d:sonar.login="${{ inputs.sonar-token }}" \ + /d:sonar.cs.opencover.reportsPaths="${{ inputs.sonar-report-paths }}" \ + /d:sonar.verbose="${{ inputs.sonar-verbose }}" + + - name: Restore NuGet packages + shell: bash + run: dotnet restore ${{ inputs.sonar-project-name }}.sln + + - name: Build Solution + shell: bash + run: dotnet build ${{ inputs.sonar-project-name }}.sln --no-restore + + - name: End SonarQube Analysis + shell: bash + run: | + dotnet-sonarscanner end \ + /d:sonar.login="${{ inputs.sonar-token }}" + + - name: Check SonarQube Quality Gate Status + id: sonarqube-status + shell: bash + continue-on-error: true + run: ${{ github.action_path }}/sonarqube-status.sh + env: + SONAR_PROJECT_KEY: ${{ inputs.sonar-project-key }} + SONAR_TOKEN: ${{ inputs.sonar-api-token }} + SONAR_HOST_URL: ${{ inputs.sonar-host-url }} + + - name: Get Coverage + shell: bash + id: coverage-calc + if: steps.sonarqube-status.outputs.status == 'OK' + continue-on-error: true + run: ${{ github.action_path }}/coverage-calc.sh + + - name: Update Quality Gates Sonarqube + shell: bash + id: quality-gates-sonarqube + if: steps.sonarqube-status.outputs.status == 'OK' + continue-on-error: true + run: ${{ github.action_path }}/quality-gates-sonarqube.sh + env: + SONAR_PROJECT_KEY: ${{ inputs.sonar-project-key }} + SONAR_TOKEN: ${{ inputs.sonar-api-token }} + SONAR_HOST_URL: ${{ inputs.sonar-host-url }} + LEGACY_COV_NUM: ${{ steps.coverage-calc.outputs.coverage }} diff --git a/sonarqube-dotnet/coverage-calc.sh b/sonarqube-dotnet/coverage-calc.sh new file mode 100755 index 0000000..b94fc77 --- /dev/null +++ b/sonarqube-dotnet/coverage-calc.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +COVERAGE=$(python -c "import xml.etree.ElementTree as ET; \ +print(float(ET.parse('coverage.xml').getroot().attrib['line-rate']) * 100)") + +echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT +echo "legacy_coverage=$COVERAGE" >> $GITHUB_OUTPUT diff --git a/sonarqube-dotnet/quality-gates-sonarqube.sh b/sonarqube-dotnet/quality-gates-sonarqube.sh new file mode 100755 index 0000000..3cb5c5f --- /dev/null +++ b/sonarqube-dotnet/quality-gates-sonarqube.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +PROJECT_RESPONSE=$(curl -s -u "$SONAR_TOKEN:" \ +"$SONAR_HOST_URL/api/qualitygates/get_by_project?project=$SONAR_PROJECT_KEY") + +QUALITY_GATE_NAME=$(echo "$PROJECT_RESPONSE" | jq -r '.qualityGate.name') +QUALITY_GATE_NAME=$(echo "$QUALITY_GATE_NAME" | jq -sRr @uri) + +echo "Quality Gate Name: $QUALITY_GATE_NAME" + +GATE_RESPONSE=$(curl -s -u "$SONAR_TOKEN:" \ +"$SONAR_HOST_URL/api/qualitygates/show?name=$QUALITY_GATE_NAME") + +NEW_COV_CONDITION_ID=$(echo "$GATE_RESPONSE" | jq -r '.conditions[] | select(.metric=="coverage") | .id') +CURRENT_NEW_COV=$(echo "$GATE_RESPONSE" | jq -r '.conditions[] | select(.metric=="coverage") | .error') + +if [ -n "$NEW_COV_CONDITION_ID" ]; then + CURRENT_NEW_COV_NUM=$(echo "$CURRENT_NEW_COV" | sed 's/[^0-9.]*//g') + + if (( $(echo "$LEGACY_COV_NUM > $CURRENT_NEW_COV_NUM" | bc -l) )); then + if (( $(echo "$LEGACY_COV_NUM > 80" | bc -l) )); then + echo "Legacy coverage ($LEGACY_COV_NUM%) > 80%, updating coverage threshold to 80%" + curl -u "$SONAR_TOKEN:" -X POST \ + "$SONAR_HOST_URL/api/qualitygates/update_condition" \ + -d "id=$NEW_COV_CONDITION_ID&metric=coverage&op=LT&error=80" + else + echo "Updating coverage threshold from $CURRENT_NEW_COV to $LEGACY_COV_NUM%" + curl -u "$SONAR_TOKEN:" -X POST \ + "$SONAR_HOST_URL/api/qualitygates/update_condition" \ + -d "id=$NEW_COV_CONDITION_ID&metric=coverage&op=LT&error=$LEGACY_COV_NUM" + fi + else + echo "Legacy coverage ($LEGACY_COV_NUM%) not greater than current coverage ($CURRENT_NEW_COV), keeping existing configuration" + fi +else + echo "Criando nova condição para coverage" + curl -u "$SONAR_TOKEN:" -X POST \ + "$SONAR_HOST_URL/api/qualitygates/create_condition" \ + -d "gateName=$QUALITY_GATE_NAME&metric=coverage&op=LT&error=$LEGACY_COV_NUM" +fi diff --git a/sonarqube-dotnet/sonarqube-status.sh b/sonarqube-dotnet/sonarqube-status.sh new file mode 100755 index 0000000..7c2d912 --- /dev/null +++ b/sonarqube-dotnet/sonarqube-status.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +sleep 15 +SONAR_STATUS=$(curl -s -u "$SONAR_TOKEN:" \ +"$SONAR_HOST_URL/api/qualitygates/project_status?projectKey=$SONAR_PROJECT_KEY") + +# Extract overall status +STATUS=$(echo "$SONAR_STATUS" | jq -r '.projectStatus.status') + +# Process failing conditions +echo "SonarQube Quality Gate Status: $STATUS" +if [ "$STATUS" != "OK" ]; then + echo "=== Failing Conditions ===" + echo "$SONAR_STATUS" | jq -r '.projectStatus.conditions[] | select(.status != "OK") | "Metric: \(.metricKey) | Threshold: \(.errorThreshold) | Actual: \(.actualValue)"' + + echo "status=$STATUS" >> $GITHUB_OUTPUT + echo "::error::Quality Gate failed with status: $STATUS" + exit 1 +fi + +echo "status=$STATUS" >> $GITHUB_OUTPUT +echo "Quality Gate passed successfully!" diff --git a/sonarqube-python/action.yml b/sonarqube-python/action.yml new file mode 100644 index 0000000..4f4e6c0 --- /dev/null +++ b/sonarqube-python/action.yml @@ -0,0 +1,107 @@ +name: 'Sonarqube Python Scanner' +description: 'Quality Gate Sonarqube for Python projects' + +inputs: + sonar-verbose: + required: false + type: boolean + default: false + sonar-sources: + required: false + type: string + default: "src" + sonar-sources-tests: + required: false + type: string + default: "tests" + sonar-test-inclusions: + required: false + type: string + default: "**/test_*.py,**/*_test.py,**/__pycache__/**,**/__init__.py" + sonar-report-paths: + required: false + type: string + default: "coverage.xml" + sonar-source-encoding: + required: false + type: string + default: "UTF-8" + sonar-exclusions: + required: false + type: string + default: "**/__pycache__/**,**/*.pyc,**/migrations/**,**/fixtures/**,**/.venv/**,**/.git/**,**/.vscode/**,**/*.html,**/htmlcov/**,coverage.xml" + sonar-lang-version: + required: false + type: string + default: "3.11" + sonar-host-url: + required: true + type: string + sonar-api-token: + required: true + type: string + sonar-token: + required: true + type: string + sonar-project-key: + required: true + type: string + sonar-timeout: + required: false + type: string + default: "60" + +runs: + using: composite + steps: + - name: SonarQube Scan + uses: sonarsource/sonarqube-scan-action@v5.2.0 + continue-on-error: true + with: + args: > + -Dsonar.projectKey=${{ inputs.sonar-project-key }} + -Dsonar.sources=${{ inputs.sonar-sources }} + -Dsonar.tests=${{ inputs.sonar-sources-tests }} + -Dsonar.test.inclusions=${{ inputs.sonar-test-inclusions }} + -Dsonar.exclusions=${{ inputs.sonar-exclusions }} + -Dsonar.python.version=${{ inputs.sonar-lang-version }} + -Dsonar.python.coverage.reportPaths=${{ inputs.sonar-report-paths }} + -Dsonar.verbose=${{ inputs.sonar-verbose }} + -Dsonar.sourceEncoding=${{ inputs.sonar-source-encoding }} + -Dsonar.scanner.connectTimeout=${{ inputs.sonar-timeout }} + -Dsonar.scanner.socketTimeout=${{ inputs.sonar-timeout }} + -Dsonar.scanner.readTimeout=${{ inputs.sonar-timeout }} + -Dsonar.scanner.skipSystemTruststore=true + -Dsonar.scanner.forceReload=true + env: + SONAR_TOKEN: ${{ inputs.sonar-token }} + SONAR_HOST_URL: ${{ inputs.sonar-host-url }} + + - name: Check SonarQube Quality Gate Status + id: sonarqube-status + shell: bash + continue-on-error: true + run: ${{ github.action_path }}/sonarqube-status.sh + env: + SONAR_PROJECT_KEY: ${{ inputs.sonar-project-key }} + SONAR_TOKEN: ${{ inputs.sonar-api-token }} + SONAR_HOST_URL: ${{ inputs.sonar-host-url }} + + - name: Get Coverage + shell: bash + id: coverage-calc + if: steps.sonarqube-status.outputs.status == 'OK' + continue-on-error: true + run: ${{ github.action_path }}/coverage-calc.sh + + - name: Update Quality Gates Sonarqube + shell: bash + id: quality-gates-sonarqube + if: steps.sonarqube-status.outputs.status == 'OK' + continue-on-error: true + run: ${{ github.action_path }}/quality-gates-sonarqube.sh + env: + SONAR_PROJECT_KEY: ${{ inputs.sonar-project-key }} + SONAR_TOKEN: ${{ inputs.sonar-api-token }} + SONAR_HOST_URL: ${{ inputs.sonar-host-url }} + LEGACY_COV_NUM: ${{ steps.coverage-calc.outputs.coverage }} diff --git a/sonarqube-python/coverage-calc.sh b/sonarqube-python/coverage-calc.sh new file mode 100755 index 0000000..b94fc77 --- /dev/null +++ b/sonarqube-python/coverage-calc.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +COVERAGE=$(python -c "import xml.etree.ElementTree as ET; \ +print(float(ET.parse('coverage.xml').getroot().attrib['line-rate']) * 100)") + +echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT +echo "legacy_coverage=$COVERAGE" >> $GITHUB_OUTPUT diff --git a/sonarqube-python/quality-gates-sonarqube.sh b/sonarqube-python/quality-gates-sonarqube.sh new file mode 100755 index 0000000..3cb5c5f --- /dev/null +++ b/sonarqube-python/quality-gates-sonarqube.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +PROJECT_RESPONSE=$(curl -s -u "$SONAR_TOKEN:" \ +"$SONAR_HOST_URL/api/qualitygates/get_by_project?project=$SONAR_PROJECT_KEY") + +QUALITY_GATE_NAME=$(echo "$PROJECT_RESPONSE" | jq -r '.qualityGate.name') +QUALITY_GATE_NAME=$(echo "$QUALITY_GATE_NAME" | jq -sRr @uri) + +echo "Quality Gate Name: $QUALITY_GATE_NAME" + +GATE_RESPONSE=$(curl -s -u "$SONAR_TOKEN:" \ +"$SONAR_HOST_URL/api/qualitygates/show?name=$QUALITY_GATE_NAME") + +NEW_COV_CONDITION_ID=$(echo "$GATE_RESPONSE" | jq -r '.conditions[] | select(.metric=="coverage") | .id') +CURRENT_NEW_COV=$(echo "$GATE_RESPONSE" | jq -r '.conditions[] | select(.metric=="coverage") | .error') + +if [ -n "$NEW_COV_CONDITION_ID" ]; then + CURRENT_NEW_COV_NUM=$(echo "$CURRENT_NEW_COV" | sed 's/[^0-9.]*//g') + + if (( $(echo "$LEGACY_COV_NUM > $CURRENT_NEW_COV_NUM" | bc -l) )); then + if (( $(echo "$LEGACY_COV_NUM > 80" | bc -l) )); then + echo "Legacy coverage ($LEGACY_COV_NUM%) > 80%, updating coverage threshold to 80%" + curl -u "$SONAR_TOKEN:" -X POST \ + "$SONAR_HOST_URL/api/qualitygates/update_condition" \ + -d "id=$NEW_COV_CONDITION_ID&metric=coverage&op=LT&error=80" + else + echo "Updating coverage threshold from $CURRENT_NEW_COV to $LEGACY_COV_NUM%" + curl -u "$SONAR_TOKEN:" -X POST \ + "$SONAR_HOST_URL/api/qualitygates/update_condition" \ + -d "id=$NEW_COV_CONDITION_ID&metric=coverage&op=LT&error=$LEGACY_COV_NUM" + fi + else + echo "Legacy coverage ($LEGACY_COV_NUM%) not greater than current coverage ($CURRENT_NEW_COV), keeping existing configuration" + fi +else + echo "Criando nova condição para coverage" + curl -u "$SONAR_TOKEN:" -X POST \ + "$SONAR_HOST_URL/api/qualitygates/create_condition" \ + -d "gateName=$QUALITY_GATE_NAME&metric=coverage&op=LT&error=$LEGACY_COV_NUM" +fi diff --git a/sonarqube-python/sonarqube-status.sh b/sonarqube-python/sonarqube-status.sh new file mode 100755 index 0000000..7c2d912 --- /dev/null +++ b/sonarqube-python/sonarqube-status.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +sleep 15 +SONAR_STATUS=$(curl -s -u "$SONAR_TOKEN:" \ +"$SONAR_HOST_URL/api/qualitygates/project_status?projectKey=$SONAR_PROJECT_KEY") + +# Extract overall status +STATUS=$(echo "$SONAR_STATUS" | jq -r '.projectStatus.status') + +# Process failing conditions +echo "SonarQube Quality Gate Status: $STATUS" +if [ "$STATUS" != "OK" ]; then + echo "=== Failing Conditions ===" + echo "$SONAR_STATUS" | jq -r '.projectStatus.conditions[] | select(.status != "OK") | "Metric: \(.metricKey) | Threshold: \(.errorThreshold) | Actual: \(.actualValue)"' + + echo "status=$STATUS" >> $GITHUB_OUTPUT + echo "::error::Quality Gate failed with status: $STATUS" + exit 1 +fi + +echo "status=$STATUS" >> $GITHUB_OUTPUT +echo "Quality Gate passed successfully!" diff --git a/tag-bump/Dockerfile b/tag-bump/Dockerfile new file mode 100644 index 0000000..01bc88c --- /dev/null +++ b/tag-bump/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine + +RUN apk --no-cache add bash git git-lfs curl jq && npm install -g semver + +COPY action.sh /action.sh + +ENTRYPOINT ["/action.sh"] diff --git a/tag-bump/action.sh b/tag-bump/action.sh new file mode 100755 index 0000000..fd4b911 --- /dev/null +++ b/tag-bump/action.sh @@ -0,0 +1,293 @@ +#!/bin/bash + +set -eo pipefail + +# config +default_semvar_bump=${DEFAULT_BUMP:-minor} +default_branch=${DEFAULT_BRANCH:-$GITHUB_BASE_REF} # get the default branch from github runner env vars +with_v=${WITH_V:-false} +release_branches=${RELEASE_BRANCHES:-master,main} +custom_tag=${CUSTOM_TAG:-} +source=${SOURCE:-.} +dryrun=${DRY_RUN:-false} +git_api_tagging=${GIT_API_TAGGING:-true} +initial_version=${INITIAL_VERSION:-0.0.0} +tag_context=${TAG_CONTEXT:-repo} +tag_prefix=${TAG_PREFIX:-false} +prerelease=${PRERELEASE:-false} +suffix=${PRERELEASE_SUFFIX:-beta} +verbose=${VERBOSE:-false} +major_string_token=${MAJOR_STRING_TOKEN:-#major} +minor_string_token=${MINOR_STRING_TOKEN:-#minor} +patch_string_token=${PATCH_STRING_TOKEN:-#patch} +none_string_token=${NONE_STRING_TOKEN:-#none} +branch_history=${BRANCH_HISTORY:-compare} +force_without_changes=${FORCE_WITHOUT_CHANGES:-false} +force_without_changes_pre=${FORCE_WITHOUT_CHANGES:-false} +tag_message=${TAG_MESSAGE:-""} + +# since https://github.blog/2022-04-12-git-security-vulnerability-announced/ runner uses? +git config --global --add safe.directory /github/workspace + +cd "${GITHUB_WORKSPACE}/${source}" || exit 1 + +echo "*** CONFIGURATION ***" +echo -e "\tDEFAULT_BUMP: ${default_semvar_bump}" +echo -e "\tDEFAULT_BRANCH: ${default_branch}" +echo -e "\tWITH_V: ${with_v}" +echo -e "\tRELEASE_BRANCHES: ${release_branches}" +echo -e "\tCUSTOM_TAG: ${custom_tag}" +echo -e "\tSOURCE: ${source}" +echo -e "\tDRY_RUN: ${dryrun}" +echo -e "\tGIT_API_TAGGING: ${git_api_tagging}" +echo -e "\tINITIAL_VERSION: ${initial_version}" +echo -e "\tTAG_CONTEXT: ${tag_context}" +echo -e "\tTAG_PREFIX: ${tag_prefix}" +echo -e "\tPRERELEASE: ${prerelease}" +echo -e "\tPRERELEASE_SUFFIX: ${suffix}" +echo -e "\tVERBOSE: ${verbose}" +echo -e "\tMAJOR_STRING_TOKEN: ${major_string_token}" +echo -e "\tMINOR_STRING_TOKEN: ${minor_string_token}" +echo -e "\tPATCH_STRING_TOKEN: ${patch_string_token}" +echo -e "\tNONE_STRING_TOKEN: ${none_string_token}" +echo -e "\tBRANCH_HISTORY: ${branch_history}" +echo -e "\tFORCE_WITHOUT_CHANGES: ${force_without_changes}" +echo -e "\tFORCE_WITHOUT_CHANGES_PRE: ${force_without_changes_pre}" +echo -e "\tTAG_MESSAGE: ${tag_message}" + +# verbose, show everything +if $verbose +then + set -x +fi + +setOutput() { + echo "${1}=${2}" >> "${GITHUB_OUTPUT}" +} + +current_branch=$(git rev-parse --abbrev-ref HEAD) + +pre_release="$prerelease" +IFS=',' read -ra branch <<< "$release_branches" +for b in "${branch[@]}"; do + # check if ${current_branch} is in ${release_branches} | exact branch match + if [[ "$current_branch" == "$b" ]] + then + pre_release="false" + fi + # verify non specific branch names like .* release/* if wildcard filter then =~ + if [ "$b" != "${b//[\[\]|.? +*]/}" ] && [[ "$current_branch" =~ $b ]] + then + pre_release="false" + fi +done +echo "pre_release = $pre_release" + +# fetch tags +git fetch --tags + +# Set no tag prefix (not even v) +tagPrefix="" + +if $with_v +then + tagPrefix="v" +fi + +# If a tag_prefix is supplied use that +if [[ "${tag_prefix}" != "false" ]] +then + tagPrefix=$tag_prefix +fi + +tagFmt="^$tagPrefix?[0-9]+\.[0-9]+\.[0-9]+$" +preTagFmt="^$tagPrefix?[0-9]+\.[0-9]+\.[0-9]+(-$suffix\.[0-9]+)$" + +# get the git refs +git_refs= +case "$tag_context" in + *repo*) + git_refs=$(git for-each-ref --sort=-v:refname --format '%(refname:lstrip=2)') + ;; + *branch*) + git_refs=$(git tag --list --merged HEAD --sort=-committerdate) + ;; + * ) echo "Unrecognised context" + exit 1;; +esac + +# get the latest tag that looks like a semver (with or without v) +matching_tag_refs=$( (grep -E "$tagFmt" <<< "$git_refs") || true) +matching_pre_tag_refs=$( (grep -E "$preTagFmt" <<< "$git_refs") || true) +tag=$(head -n 1 <<< "$matching_tag_refs") +pre_tag=$(head -n 1 <<< "$matching_pre_tag_refs") + +# if there are none, start tags at initial version +if [ -z "$tag" ] +then + tag="$tagPrefix$initial_version" + if [ -z "$pre_tag" ] && $pre_release + then + pre_tag="$tagPrefix$initial_version" + fi +fi + +# get current commit hash for tag +tag_commit=$(git rev-list -n 1 "$tag" || true ) +# get current commit hash +commit=$(git rev-parse HEAD) +# skip if there are no new commits for non-pre_release +if [ "$tag_commit" == "$commit" ] && [ "$force_without_changes" == "false" ] +then + echo "No new commits since previous tag. Skipping..." + setOutput "new_tag" "$tag" + setOutput "tag" "$tag" + exit 0 +fi + +# sanitize that the default_branch is set (via env var when running on PRs) else find it natively +if [ -z "${default_branch}" ] && [ "$branch_history" == "full" ] +then + echo "The DEFAULT_BRANCH should be autodetected when tag-action runs on on PRs else must be defined, See: https://github.com/anothrNick/github-tag-action/pull/230, since is not defined we find it natively" + default_branch=$(git branch -rl '*/master' '*/main' | cut -d / -f2) + echo "default_branch=${default_branch}" + # re check this + if [ -z "${default_branch}" ] + then + echo "::error::DEFAULT_BRANCH must not be null, something has gone wrong." + exit 1 + fi +fi + +# get the merge commit message looking for #bumps +declare -A history_type=( + ["last"]="$(git show -s --format=%B)" \ + ["full"]="$(git log "${default_branch}"..HEAD --format=%B)" \ + ["compare"]="$(git log "${tag_commit}".."${commit}" --format=%B)" \ +) +log=${history_type[${branch_history}]} +printf "History:\n---\n%s\n---\n" "$log" + +if [ -z "$tagPrefix" ] +then + current_tag=${tag} +else + current_tag="$(echo ${tag}| sed "s/${tagPrefix}//g")" +fi +case "$log" in + *$major_string_token* ) new=${tagPrefix}$(semver -i major "${current_tag}"); part="major";; + *$minor_string_token* ) new=${tagPrefix}$(semver -i minor "${current_tag}"); part="minor";; + *$patch_string_token* ) new=${tagPrefix}$(semver -i patch "${current_tag}"); part="patch";; + *$none_string_token* ) + echo "Default bump was set to none. Skipping..." + setOutput "old_tag" "$tag" + setOutput "new_tag" "$tag" + setOutput "tag" "$tag" + setOutput "part" "$default_semvar_bump" + exit 0;; + * ) + if [ "$default_semvar_bump" == "none" ] + then + echo "Default bump was set to none. Skipping..." + setOutput "old_tag" "$tag" + setOutput "new_tag" "$tag" + setOutput "tag" "$tag" + setOutput "part" "$default_semvar_bump" + exit 0 + else + new=${tagPrefix}$(semver -i "${default_semvar_bump}" "${current_tag}") + part=$default_semvar_bump + fi + ;; +esac + +if $pre_release +then + # get current commit hash for tag + pre_tag_commit=$(git rev-list -n 1 "$pre_tag" || true) + # skip if there are no new commits for pre_release + if [ "$pre_tag_commit" == "$commit" ] && [ "$force_without_changes_pre" == "false" ] + then + echo "No new commits since previous pre_tag. Skipping..." + setOutput "new_tag" "$pre_tag" + setOutput "tag" "$pre_tag" + exit 0 + fi + # already a pre-release available, bump it + if [[ "$pre_tag" =~ $new ]] && [[ "$pre_tag" =~ $suffix ]] + then + new=${tagPrefix}$(semver -i prerelease "${pre_tag}" --preid "${suffix}") + echo -e "Bumping ${suffix} pre-tag ${pre_tag}. New pre-tag ${new}" + else + new="${tagPrefix}${new}-${suffix}.0" + echo -e "Setting ${suffix} pre-tag ${pre_tag} - With pre-tag ${new}" + fi + part="pre-$part" +else + echo -e "Bumping tag ${tag} - New tag ${new}" +fi + +# as defined in readme if CUSTOM_TAG is used any semver calculations are irrelevant. +if [ -n "$custom_tag" ] +then + new="$custom_tag" +fi + +# set outputs +setOutput "new_tag" "$new" +setOutput "part" "$part" +setOutput "tag" "$new" # this needs to go in v2 is breaking change +setOutput "old_tag" "$tag" + +# dry run exit without real changes +if $dryrun +then + exit 0 +fi + +# Modify the tag creation part +if [ -n "$tag_message" ] +then + echo "EVENT: creating local tag $new with message: $tag_message" + git tag -a "$new" -m "$tag_message" || exit 1 +else + echo "EVENT: creating local tag $new" + git tag -f "$new" || exit 1 +fi + +echo "EVENT: pushing tag $new to origin" + +if $git_api_tagging +then + # use git api to push + dt=$(date '+%Y-%m-%dT%H:%M:%SZ') + full_name=$GITHUB_REPOSITORY + git_refs_url=$(jq .repository.git_refs_url "$GITHUB_EVENT_PATH" | tr -d '"' | sed 's/{\/sha}//g') + + echo "$dt: **pushing tag $new to repo $full_name" + + git_refs_response=$( + curl -s -X POST "$git_refs_url" \ + -H "Authorization: token $GITHUB_TOKEN" \ + -d @- << EOF +{ + "ref": "refs/tags/$new", + "sha": "$commit" +} +EOF +) + + git_ref_posted=$( echo "${git_refs_response}" | jq .ref | tr -d '"' ) + + echo "::debug::${git_refs_response}" + if [ "${git_ref_posted}" = "refs/tags/${new}" ] + then + exit 0 + else + echo "::error::Tag was not created properly." + exit 1 + fi +else + # use git cli to push + git push -f origin "$new" || exit 1 +fi diff --git a/tag-bump/action.yml b/tag-bump/action.yml new file mode 100644 index 0000000..ac1e1f2 --- /dev/null +++ b/tag-bump/action.yml @@ -0,0 +1,13 @@ +name: Create Tag +description: Create Tag to be reused by rollback feature + +runs: + using: 'docker' + image: 'Dockerfile' +outputs: + new_tag: + description: 'Generated tag' + tag: + description: 'The latest tag after running this action' + part: + description: 'The part of version which was bumped'