Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/deploy/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Deploy step
description: One step of a deploy or rollback to Laravel Forge or Laravel Cloud, with the release history kept in refs/datalogix/*. The deploy workflow sets the environment it reads (MODE, PROVIDER, ENVIRONMENT, DEPLOY_HOOK, HEALTH_URL, LANGUAGE and the step's own values).
description: One step of a deploy or rollback to a provider in providers/ (Laravel Forge, Laravel Cloud), with the release history kept in refs/datalogix/*. The deploy workflow sets the environment it reads (MODE, PROVIDER, ENVIRONMENT, DEPLOY_HOOK, HEALTH_URL, LANGUAGE and the step's own values).

inputs:
step:
Expand Down
105 changes: 71 additions & 34 deletions .github/actions/deploy/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,32 @@ source "$(dirname "${BASH_SOURCE[0]}")/../lib/github.sh"
source "$(dirname "${BASH_SOURCE[0]}")/../lib/inputs.sh"
# shellcheck source=SCRIPTDIR/../lib/releases.sh
source "$(dirname "${BASH_SOURCE[0]}")/../lib/releases.sh"

# The deploy provider (providers/<id>.sh) defines provider_settle_seconds (prints the seconds a deploy usually takes, or
# the most provider_wait <seconds> follows it, when the provider can tell it finished), provider_check and
# provider_trigger <sha>,
# which returns non-zero, with an ::error::, when the provider did not accept the deploy. The name is checked before it
# becomes a path
providers_dir="$(dirname "${BASH_SOURCE[0]}")/providers"
if [[ ! "${PROVIDER:-}" =~ ^[a-z][a-z0-9-]*$ ]] || [ ! -f "$providers_dir/$PROVIDER.sh" ]; then
known=""
for file in "$providers_dir"/*.sh; do known+="${known:+, }$(basename "$file" .sh)"; done
echo "::error::provider must be one of $known, got '${PROVIDER:-}'"
exit 1
fi
# shellcheck source=/dev/null
source "$providers_dir/$PROVIDER.sh"

# The monitoring service (monitoring/<id>.sh) defines monitoring_name and, with deploy markers, mark_<id> <sha> <name>,
# which marks a deploy when the environment has its token and only warns when it cannot. none, or a service without a
# script, has neither
monitoring=${MONITORING:-nightwatch}
monitoring_file="$(dirname "${BASH_SOURCE[0]}")/monitoring/$monitoring.sh"
if [[ "$monitoring" =~ ^[a-z][a-z0-9-]*$ ]] && [ -f "$monitoring_file" ]; then
# shellcheck source=/dev/null
source "$monitoring_file"
fi

history=$(history_ref "$environment_key")
rejected=$(rolled_back_ref "$environment_key")
live_path=$(live_ref "$environment_key")
Expand Down Expand Up @@ -49,31 +75,42 @@ mark_rolled_back() {
gh api "repos/$GITHUB_REPOSITORY/git/refs" -f "ref=refs/$rejected/$1" -f "sha=$1" >/dev/null 2>&1 || true
}

trigger() {
local sha=$1 code
if [ "$PROVIDER" = cloud ]; then
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 60 -X POST --get --data-urlencode "commit_hash=$sha" "$DEPLOY_HOOK" || true)
else
# Forge deploys the branch head; the commit variable makes the deploy script check out this exact commit
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 60 -X POST --get \
--data-urlencode "forge_deploy_branch=$BRANCH" \
--data-urlencode "forge_deploy_commit=$sha" \
--data-urlencode "commit=$sha" \
"$DEPLOY_HOOK" || true)
# For providers deployed by a hook: the DEPLOY_HOOK secret is required
require_deploy_hook() {
if [ -z "${DEPLOY_HOOK:-}" ]; then
echo "::error::Add the DEPLOY_HOOK secret to the $ENVIRONMENT environment (Settings > Environments)"
exit 1
fi
# No retries: a hook that answered late may have deployed already
}

# POSTs to the deploy hook with the given query parameters (curl --data-urlencode ...). No retries: a hook that
# answered late may have deployed already
post_hook() {
local code
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 60 -X POST --get "$@" "$DEPLOY_HOOK" || true)
if [ "${code:0:1}" != 2 ]; then
echo "::error::The deploy hook answered HTTP ${code:-000}"
return 1
fi
}

trigger() {
local sha=$1
provider_trigger "$sha" || return 1
set_live "$sha"
echo "Deploy of $sha to $ENVIRONMENT triggered"
}

settle() {
local seconds=${1:-0}
if [ "$seconds" -eq 0 ]; then
if [ "$PROVIDER" = cloud ]; then seconds=300; else seconds=120; fi
seconds=$(provider_settle_seconds)
fi
# A provider that can tell when the deploy finished follows it, for at most those seconds
if declare -F provider_wait >/dev/null; then
echo "Waiting up to $seconds seconds for $PROVIDER to finish the deploy"
provider_wait "$seconds"
return
fi
echo "Waiting $seconds seconds for $PROVIDER to finish the deploy"
sleep "$seconds"
Expand Down Expand Up @@ -103,30 +140,17 @@ watch_health() {
done
}

# Laravel Cloud already reports its deploys to Nightwatch
report_nightwatch() {
local sha=$1 name=$2
[ "$PROVIDER" = forge ] && [ -n "${NIGHTWATCH_TOKEN:-}" ] || return 0
# The token goes through a header file, never in the command line
jq -n --arg deploy "$sha" --arg ref "$sha" --arg name "$name" --arg url "$RUN_URL" '{deploy: $deploy, ref: $ref, name: $name, url: $url}' |
curl -sS --fail --max-time 30 -o /dev/null -X POST https://nightwatch.laravel.com/api/deployments \
-H @<(printf 'Authorization: Bearer %s\n' "$NIGHTWATCH_TOKEN") -H 'Content-Type: application/json' --data @- ||
echo "::warning::Could not report the deploy to Nightwatch"
# Marks the deploy in the monitoring service, when it has deploy markers
report_markers() {
if declare -F "mark_$monitoring" >/dev/null; then "mark_$monitoring" "$1" "$2"; fi
}

check() {
case "$PROVIDER" in
forge | cloud) ;;
*) echo "::error::provider must be forge or cloud, got '$PROVIDER'"; exit 1 ;;
esac
case "$MODE" in
deploy | rollback) ;;
*) echo "::error::mode must be deploy or rollback, got '$MODE'"; exit 1 ;;
esac
if [ -z "$DEPLOY_HOOK" ]; then
echo "::error::Add the DEPLOY_HOOK secret to the $ENVIRONMENT environment (Settings > Environments)"
exit 1
fi
provider_check
if [ -z "$HEALTH_URL" ]; then
echo "::error::Add the HEALTH_URL variable to the $ENVIRONMENT environment, e.g. https://example.com/up"
exit 1
Expand Down Expand Up @@ -191,7 +215,7 @@ deploy() {
else
name="$ENVIRONMENT ${SHA:0:7}"
fi
report_nightwatch "$SHA" "$name"
report_markers "$SHA" "$name"
}

watch() {
Expand Down Expand Up @@ -226,7 +250,7 @@ rollback() {
# Only now that the environment changed, as in a rollback by label: a release recorded as good before (deployed
# again with Re-run jobs) is never a rollback target after failing
mark_rolled_back "$GITHUB_SHA"
report_nightwatch "$target" "$ENVIRONMENT rollback to ${target:0:7}"
report_markers "$target" "$ENVIRONMENT rollback to ${target:0:7}"

settle "${SETTLE:-0}"
if watch_health 3; then
Expand All @@ -238,6 +262,20 @@ rollback() {
fi
}

# Where to look after a failed deploy: the monitoring service's errors, when there is one, and the provider's logs
errors_hint() {
local rerun_en="To redeploy this commit after the fix, use **Re-run jobs** on this run: $RUN_URL"
local rerun_pt="Para redeployar este commit depois da correção, use **Re-run jobs** nesta execução: $RUN_URL"
if declare -F monitoring_name >/dev/null; then
local name
name=$(monitoring_name)
t "Check the errors in $name and the deploy logs in $PROVIDER. $rerun_en" \
"Verifique os erros no $name e os logs do deploy no $PROVIDER. $rerun_pt"
else
t "Check the deploy logs in $PROVIDER. $rerun_en" "Verifique os logs do deploy no $PROVIDER. $rerun_pt"
fi
}

open_issue() {
local short=${GITHUB_SHA:0:7} result body
# shellcheck disable=SC2153 # TARGET is set by the step env in deploy.yml
Expand All @@ -261,8 +299,7 @@ open_issue() {
"$(t "The deploy of commit $GITHUB_SHA to **$ENVIRONMENT** failed the health checks of \`$HEALTH_URL\`." \
"O deploy do commit $GITHUB_SHA em **$ENVIRONMENT** falhou nos health checks de \`$HEALTH_URL\`.")" \
"$result" \
"$(t "Check the errors in Nightwatch and the deploy logs in $PROVIDER. To redeploy this commit after the fix, use **Re-run jobs** on this run: $RUN_URL" \
"Verifique os erros no Nightwatch e os logs do deploy no $PROVIDER. Para redeployar este commit depois da correção, use **Re-run jobs** nesta execução: $RUN_URL")" \
"$(errors_hint)" \
"$(t "To ask Claude to investigate, add the \`claude-plan\` label." \
"Para pedir ao Claude que investigue, adicione o label \`claude-plan\`.")")

Expand Down
6 changes: 6 additions & 0 deletions .github/actions/deploy/monitoring/inspector.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# shellcheck shell=bash
# Inspector: named in the issue of a failed deploy; no deploy markers (Inspector's API documents none)

monitoring_name() {
echo Inspector
}
17 changes: 17 additions & 0 deletions .github/actions/deploy/monitoring/nightwatch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# shellcheck shell=bash
# Laravel Nightwatch, with deploy markers sent with the environment's NIGHTWATCH_TOKEN. Laravel Cloud already reports
# its deploys to Nightwatch, so only the other providers mark them here

monitoring_name() {
echo Nightwatch
}

mark_nightwatch() {
local sha=$1 name=$2
[ "$PROVIDER" != cloud ] && [ -n "${NIGHTWATCH_TOKEN:-}" ] || return 0
# The token goes through a header file, never in the command line
jq -n --arg deploy "$sha" --arg ref "$sha" --arg name "$name" --arg url "$RUN_URL" '{deploy: $deploy, ref: $ref, name: $name, url: $url}' |
curl -sS --fail --max-time 30 -o /dev/null -X POST https://nightwatch.laravel.com/api/deployments \
-H @<(printf 'Authorization: Bearer %s\n' "$NIGHTWATCH_TOKEN") -H 'Content-Type: application/json' --data @- ||
echo "::warning::Could not report the deploy to Nightwatch"
}
7 changes: 7 additions & 0 deletions .github/actions/deploy/monitoring/sentry.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# shellcheck shell=bash
# Sentry: named in the issue of a failed deploy; no deploy markers yet (Sentry's release API needs the organization
# and project slugs)

monitoring_name() {
echo Sentry
}
15 changes: 15 additions & 0 deletions .github/actions/deploy/providers/cloud.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# shellcheck shell=bash
# Laravel Cloud: the environment's deploy hook, which deploys the commit it is given

# Cloud builds an image first
provider_settle_seconds() {
echo 300
}

provider_check() {
require_deploy_hook
}

provider_trigger() {
post_hook --data-urlencode "commit_hash=$1"
}
19 changes: 19 additions & 0 deletions .github/actions/deploy/providers/forge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# shellcheck shell=bash
# Laravel Forge: the site's deploy hook. Forge deploys the branch head; the commit variable makes the deploy script
# check out this exact commit (setup adds that check to the script)

# Seconds a deploy usually takes, waited before watching when settle-seconds is 0
provider_settle_seconds() {
echo 120
}

provider_check() {
require_deploy_hook
}

provider_trigger() {
post_hook \
--data-urlencode "forge_deploy_branch=$BRANCH" \
--data-urlencode "forge_deploy_commit=$1" \
--data-urlencode "commit=$1"
}
91 changes: 91 additions & 0 deletions .github/actions/deploy/providers/vercel.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# shellcheck shell=bash
# Vercel: a deployment of the exact commit through the REST API, with the environment's VERCEL_TOKEN secret and its
# VERCEL_PROJECT variable (the project's id or name; VERCEL_TEAM_ID too for a team's project). The GitHub environment's
# name is the Vercel target (production, staging or a custom environment) unless VERCEL_TARGET names another. The wait
# follows the deployment until it is ready, instead of a fixed time

# shellcheck disable=SC2154 # environment_key is set by deploy.sh, which sources this file
vercel_deployment_file="${RUNNER_TEMP:-/tmp}/datalogix-vercel-$environment_key"

# The most the wait follows a build
provider_settle_seconds() {
echo 900
}

provider_check() {
if [ -z "${VERCEL_TOKEN:-}" ]; then
echo "::error::Add the VERCEL_TOKEN secret to the $ENVIRONMENT environment (Settings > Environments)"
exit 1
fi
if [ -z "${VERCEL_PROJECT:-}" ]; then
echo "::error::Add the VERCEL_PROJECT variable (the Vercel project's id or name) to the $ENVIRONMENT environment"
exit 1
fi
}

# Calls the API (method, path, file for the body, then curl arguments) and prints the HTTP code. The token goes through
# a header file, never in the command line
vercel_call() {
local method=$1 path=$2 out=$3 url
shift 3
url="https://api.vercel.com$path"
if [ -n "${VERCEL_TEAM_ID:-}" ]; then
if [[ "$url" == *\?* ]]; then url+="&teamId=$VERCEL_TEAM_ID"; else url+="?teamId=$VERCEL_TEAM_ID"; fi
fi
curl -sS -o "$out" -w '%{http_code}' --max-time 60 -X "$method" \
-H @<(printf 'Authorization: Bearer %s\n' "$VERCEL_TOKEN") "$@" "$url" || true
}

provider_trigger() {
local sha=$1 out code id
out=$(mktemp)
# forceNew: a rollback deploys a commit Vercel already built, which it would otherwise answer with that deployment
# shellcheck disable=SC2154 # environment_key is set by deploy.sh
code=$(jq -n --arg project "$VERCEL_PROJECT" --arg target "${VERCEL_TARGET:-$environment_key}" \
--arg org "${GITHUB_REPOSITORY%%/*}" --arg repo "${GITHUB_REPOSITORY#*/}" --arg ref "$BRANCH" --arg sha "$sha" \
'{name: $project, project: $project, target: $target, gitSource: {type: "github", org: $org, repo: $repo, ref: $ref, sha: $sha}}' |
vercel_call POST "/v13/deployments?forceNew=1" "$out" -H 'Content-Type: application/json' --data @-)
if [ "${code:0:1}" != 2 ]; then
echo "::error::Vercel answered HTTP ${code:-000}: $(jq -r '.error.message // empty' "$out" 2>/dev/null)"
return 1
fi
id=$(jq -r '.id // empty' "$out" 2>/dev/null)
if [ -z "$id" ]; then
echo "::error::Vercel did not return the deployment"
return 1
fi
# The wait runs in another step of the same job
echo "$id" > "$vercel_deployment_file"
echo "Vercel deployment $id"
}

# Follows the deployment every 10 seconds until it is ready, for at most the given seconds. A build that fails leaves
# the previous deployment live, so there is nothing to roll back: the step fails
provider_wait() {
local limit=$1 waited=0 id out code state
id=$(cat "$vercel_deployment_file" 2>/dev/null) || true
if [ -z "$id" ]; then
echo "::error::No Vercel deployment to wait for"
return 1
fi
out=$(mktemp)
while true; do
code=$(vercel_call GET "/v13/deployments/$id" "$out")
# A failed read is tried again: the deployment goes on
if [ "${code:0:1}" = 2 ]; then
state=$(jq -r '.readyState // empty' "$out" 2>/dev/null)
case "$state" in
READY) echo "Vercel deployment $id is ready"; return 0 ;;
ERROR | CANCELED)
echo "::error::The Vercel deployment $id ended as $state; the previous deployment is still live"
return 1 ;;
esac
fi
if [ "$waited" -ge "$limit" ]; then
echo "::error::The Vercel deployment $id was not ready after $limit seconds (${state:-unknown})"
return 1
fi
sleep 10
waited=$((waited + 10))
done
}
Loading
Loading