diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b0b8b10..cb4f9e5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -77,3 +77,14 @@ jobs: VERSION: ${{ github.ref_name }} NAMESPACE: ${{ vars.K8S_NAMESPACE }} run: ./ci/k8s-rollout.sh + + - name: Register release + deploy in Sentry + # Runs after the rollout reaches Ready. Marks the release as + # deployed to this environment so Sentry's release timeline + + # suspect-commits attribution have an anchor for this version. + env: + VERSION: ${{ github.ref_name }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ENVIRONMENT: ${{ inputs.environment }} + COMMIT_SHA: ${{ github.sha }} + run: ./ci/notify-sentry-release.sh diff --git a/ci/notify-sentry-release.sh b/ci/notify-sentry-release.sh new file mode 100755 index 0000000..22c7bd7 --- /dev/null +++ b/ci/notify-sentry-release.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Tells Sentry that VERSION was just deployed to ENVIRONMENT. Two API calls: +# +# 1. POST /api/0/organizations//releases/ +# Creates the release if it doesn't exist. Includes the git commit SHA +# via `refs` so Sentry can compute commits-since-last-release and run +# its "suspect commits" attribution on new errors. +# +# 2. POST /api/0/organizations//releases//deploys/ +# Records the deploy event so it shows up on Sentry's release timeline +# keyed by environment. Sentry treats repeat calls as new deploys, so +# this fires once per deploy.yml run. +# +# Required env vars: +# VERSION — release tag, e.g. 0.1.10. +# SENTRY_AUTH_TOKEN — token from a Sentry Internal Integration with +# scopes org:read + project:releases. +# SENTRY_ENVIRONMENT — environment name, e.g. production. +# +# Optional env vars: +# SENTRY_ORG — organization slug (default: riotbyte). +# SENTRY_PROJECT — project slug (default: cv-generator-api). +# SENTRY_REPO — repo slug for commit attribution (default: +# riotbyte-com/cv-generator). Set empty to skip refs. +# COMMIT_SHA — git SHA of the released commit (default: HEAD). + +: "${VERSION:?VERSION is required}" +: "${SENTRY_AUTH_TOKEN:?SENTRY_AUTH_TOKEN is required}" +: "${SENTRY_ENVIRONMENT:?SENTRY_ENVIRONMENT is required}" + +SENTRY_ORG="${SENTRY_ORG:-riotbyte}" +SENTRY_PROJECT="${SENTRY_PROJECT:-cv-generator-api}" +SENTRY_REPO="${SENTRY_REPO-riotbyte-com/cv-generator}" +COMMIT_SHA="${COMMIT_SHA:-$(git rev-parse HEAD)}" + +# Build the release-create payload. `refs` is optional; if SENTRY_REPO is +# blank, omit it so Sentry doesn't 400 on an unknown repo slug. +if [ -n "${SENTRY_REPO}" ]; then + refs_json=", \"refs\": [{\"repository\": \"${SENTRY_REPO}\", \"commit\": \"${COMMIT_SHA}\"}]" +else + refs_json="" +fi + +release_body="{\"version\": \"${VERSION}\", \"projects\": [\"${SENTRY_PROJECT}\"]${refs_json}}" +deploy_body="{\"environment\": \"${SENTRY_ENVIRONMENT}\"}" + +base="https://sentry.io/api/0/organizations/${SENTRY_ORG}" +auth_header="Authorization: Bearer ${SENTRY_AUTH_TOKEN}" +ct_header="Content-Type: application/json" + +call_sentry() { + local label=$1 url=$2 body=$3 + local response_file http_code + response_file=$(mktemp) + http_code=$(curl -sS -o "${response_file}" -w "%{http_code}" \ + -X POST -H "${auth_header}" -H "${ct_header}" \ + --data "${body}" "${url}") + + case "${http_code}" in + 20[01]) + echo "${label}: ok (${http_code})" + ;; + 208|409) + # 208 Already Reported / 409 Conflict — release already exists. + # Idempotent re-runs land here; not a failure. + echo "${label}: already exists (${http_code}), continuing" + ;; + *) + echo "::error::${label} failed (${http_code})" + cat "${response_file}" + rm -f "${response_file}" + exit 1 + ;; + esac + rm -f "${response_file}" +} + +call_sentry "create release ${VERSION}" "${base}/releases/" "${release_body}" +call_sentry "register deploy ${VERSION} -> ${SENTRY_ENVIRONMENT}" \ + "${base}/releases/${VERSION}/deploys/" "${deploy_body}"