Skip to content
Merged
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
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,15 @@ deploy:
comment_provider: gitlab
```

On GitLab, the plugin **updates** the merge-request comment on each run instead of adding a new comment every time (see [GitLab MR comments](#gitlab-merge-request-comments) below). On GitHub, each run still adds a **new** PR comment via `gh pr comment`; update-in-place for GitHub is not implemented in this plugin yet.

### Self-Hosted GitLab

For self-hosted GitLab environments where `ENV0_PR_SOURCE_REPOSITORY` is not available, the plugin falls back to `ENV0_TEMPLATE_REPOSITORY` to derive the GitLab API URL for posting merge request comments.

For authentication, the plugin uses `GITLAB_TOKEN` if set, otherwise falls back to `ENV0_VCS_ACCESS_TOKEN`. The token must have the GitLab `api` scope to post MR comments.
For authentication, the plugin uses `GITLAB_TOKEN` if set, otherwise falls back to `ENV0_VCS_ACCESS_TOKEN`.

**GitLab token scopes:** The plugin calls `GET /api/v4/user` (needs **`read_user`**, or a broader scope that includes it) and lists/creates/updates merge request notes (needs **`api`** or equivalent project access with comment permissions). A classic personal access token with the **`api`** scope satisfies both. Narrower token setups must include at least `read_user` plus whatever GitLab requires for MR note read/write on your instance.

```yaml
version: 2
Expand Down Expand Up @@ -228,7 +232,7 @@ deploy:
- `submit-plan`: Uses `$ENV0_TF_PLAN_JSON` to submit the Terraform plan
- `start-change`: Marks the beginning of a change with a ticket link to the env0 deployment
- `end-change`: Marks the completion of a change with a ticket link to the env0 deployment
- `wait-for-simulation`: Retrieves Overmind simulation results as Markdown and (when `post_comment=true`) posts them to the GitHub PR or GitLab MR (automatically detected based on repository URL)
- `wait-for-simulation`: Retrieves Overmind simulation results as Markdown and (when `post_comment=true`) posts them to the GitHub PR or GitLab MR per `comment_provider` (GitLab updates the comment in place).

4. **Ticket Links**: When `ENV0_PR_NUMBER` is set (i.e., the deployment is triggered by a PR/MR), the plugin constructs a stable merge request URL from `ENV0_PR_SOURCE_REPOSITORY` (or `ENV0_TEMPLATE_REPOSITORY` as a fallback) and `ENV0_PR_NUMBER`. This ensures multiple plans for the same MR update the same Overmind change. For non-PR deployments, the ticket link falls back to the env0 deployment URL.

Expand All @@ -252,7 +256,7 @@ deploy:
- `source:write`

- GitHub authentication for the CLI when `wait-for-simulation` posts to GitHub (set `GH_TOKEN`).
- GitLab authentication when `wait-for-simulation` posts to GitLab (set `GITLAB_TOKEN`, or rely on `ENV0_VCS_ACCESS_TOKEN` which is automatically available in self-hosted GitLab environments). The token must have the GitLab `api` scope.
- GitLab authentication when `wait-for-simulation` posts to GitLab (set `GITLAB_TOKEN`, or rely on `ENV0_VCS_ACCESS_TOKEN` which is automatically available in self-hosted GitLab environments). See [Self-Hosted GitLab](#self-hosted-gitlab) for minimum scopes (`api` on a classic PAT is sufficient).

### Creating a `GH_TOKEN`

Expand All @@ -269,12 +273,25 @@ deploy:

1. Sign in to GitLab and navigate to your user settings (or group/project settings for project/group tokens).
2. Go to **Access Tokens** (or **Preferences** > **Access Tokens** for user tokens).
3. Create a new token with the `api` scope (read/write is needed to comment on merge requests).
3. Create a new token with the `api` scope (covers `GET /api/v4/user` and merge request note APIs used by this plugin).
4. Generate the token and copy it immediately—GitLab will not show it again.
5. Store the token securely in env0 (for example as an environment variable or secret) and expose it to the plugin as `GITLAB_TOKEN`.

**Note**: When `post_comment=true`, you must set `comment_provider` to `github` or `gitlab`.

### GitLab merge request comments

To **update** the same MR note on each deployment, the plugin looks for a non-system note authored by the token’s user whose body contains the HTML marker `<!-- overmind-change-summary -->`. Overmind’s default markdown includes this marker; if it is missing, the plugin **warns**, **appends** the marker to the posted body, and subsequent runs can match and update that note.

### Pinning plugin version (rollback)

env0 resolves `use: https://github.com/org/env0-plugin` to the default branch unless you pin a revision. To avoid picking up breaking changes—or to roll back after an upgrade—append **`@<ref>`** to the URL (tag, branch, or commit SHA), per [env0’s plugin documentation](https://docs.env0.com/docs/plugins):

```yaml
use: https://github.com/overmindtech/env0-plugin@v1.2.3
# or: https://github.com/overmindtech/env0-plugin@abc1234deadbeef...
```

## Notes

- The plugin automatically detects the operating system and architecture to download the correct Overmind CLI binary.
Expand Down
164 changes: 140 additions & 24 deletions env0.plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ name: Overmind
icon: https://avatars.githubusercontent.com/u/93521445
inputs:
action:
description: "The action to perform. Must be one of: submit-plan, start-change, end-change, wait-for-simulation"
description: 'The action to perform. Must be one of: submit-plan, start-change, end-change, wait-for-simulation'
required: true
api_key:
description: "Overmind API key for authentication"
description: 'Overmind API key for authentication'
required: true
tags:
description: "A comma-separated list of key=value tags to attach to the change (only used with submit-plan action)"
description: 'A comma-separated list of key=value tags to attach to the change (only used with submit-plan action)'
required: false
app:
description: "The Overmind instance to connect to (defaults to https://app.overmind.tech)"
description: 'The Overmind instance to connect to (defaults to https://app.overmind.tech)'
required: false
post_comment:
description: "Whether wait-for-simulation should post the Overmind markdown to the PR/MR. Defaults to true when ENV0_PR_NUMBER is set, otherwise false. When true, comment_provider must be set."
description: 'Whether wait-for-simulation should post the Overmind markdown to the PR/MR. Defaults to true when ENV0_PR_NUMBER is set, otherwise false. When true, comment_provider must be set.'
required: false
comment_provider:
description: "Where to post PR/MR comments when wait-for-simulation runs with post_comment=true. Must be one of: github, gitlab."
description: 'Where to post PR/MR comments when wait-for-simulation runs with post_comment=true. Must be one of: github, gitlab.'
required: false
on_failure:
description: "Behavior when an error occurs. 'fail' (default) fails the plugin step and blocks the deployment; 'pass' allows the deployment to continue even if this step errors."
Expand Down Expand Up @@ -47,6 +47,15 @@ run:
if [ -n "${JSON_PAYLOAD_FILE}" ] && [ -f "${JSON_PAYLOAD_FILE}" ]; then
rm -f "${JSON_PAYLOAD_FILE}"
fi
if [ -n "${GITLAB_NOTES_PAGE_FILE}" ] && [ -f "${GITLAB_NOTES_PAGE_FILE}" ]; then
rm -f "${GITLAB_NOTES_PAGE_FILE}"
fi
if [ -n "${GITLAB_FALLBACK_BODY_FILE}" ] && [ -f "${GITLAB_FALLBACK_BODY_FILE}" ]; then
rm -f "${GITLAB_FALLBACK_BODY_FILE}"
fi
if [ -n "${GITLAB_USER_FILE}" ] && [ -f "${GITLAB_USER_FILE}" ]; then
rm -f "${GITLAB_USER_FILE}"
fi
}
main_script() {
set -e
Expand All @@ -56,6 +65,9 @@ run:
GH_BINARY=""
MARKDOWN_FILE=""
JSON_PAYLOAD_FILE=""
GITLAB_NOTES_PAGE_FILE=""
GITLAB_FALLBACK_BODY_FILE=""
GITLAB_USER_FILE=""
ORIGINAL_DIR=$(pwd)

# Export API key as environment variable
Expand Down Expand Up @@ -371,7 +383,12 @@ run:

case "${comment_provider}" in
gitlab)
# GitLab merge request
# GitLab merge request — update existing Overmind note when possible
OVERMIND_MARKER='<!-- overmind-change-summary -->'
if ! grep -qF "${OVERMIND_MARKER}" "${MARKDOWN_FILE}"; then
echo "Warning: Overmind markdown lacks ${OVERMIND_MARKER}; appending it so this plugin can find and update this MR comment on future runs."
printf '\n\n%s\n' "${OVERMIND_MARKER}" >> "${MARKDOWN_FILE}"
fi

# Fall back to ENV0_TEMPLATE_REPOSITORY for self-hosted GitLab
# where ENV0_PR_SOURCE_REPOSITORY is not available
Expand Down Expand Up @@ -420,27 +437,126 @@ run:
MR_IID="${ENV0_PR_NUMBER}"
API_URL="${GITLAB_BASE_URL}/api/v4/projects/${PROJECT_PATH_ENCODED}/merge_requests/${MR_IID}/notes"

echo "Posting simulation results to GitLab MR ${PROJECT_PATH}!${MR_IID}..."

# Create JSON payload file using jq
JSON_PAYLOAD_FILE=$(mktemp)
jq -n --rawfile body "${MARKDOWN_FILE}" '{body: $body}' > "${JSON_PAYLOAD_FILE}"

# Post comment using curl
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
GITLAB_USER_FILE=$(mktemp)
if ! curl -fsSL \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary "@${JSON_PAYLOAD_FILE}" \
"${API_URL}")
"${GITLAB_BASE_URL}/api/v4/user" \
-o "${GITLAB_USER_FILE}"; then
echo "Error: GitLab GET /api/v4/user failed (check token and ${GITLAB_BASE_URL})"
rm -f "${GITLAB_USER_FILE}"
GITLAB_USER_FILE=""
exit 1
fi
if ! GITLAB_USER_ID=$(jq -e -r '.id' "${GITLAB_USER_FILE}"); then
echo "Error: GitLab /user response missing id (bad token or wrong API URL?)"
rm -f "${GITLAB_USER_FILE}"
GITLAB_USER_FILE=""
exit 1
fi
rm -f "${GITLAB_USER_FILE}"
GITLAB_USER_FILE=""

# Clean up JSON payload file
rm -f "${JSON_PAYLOAD_FILE}"
echo "Posting simulation results to GitLab MR ${PROJECT_PATH}!${MR_IID}..."

if [ "${HTTP_CODE}" -ge 200 ] && [ "${HTTP_CODE}" -lt 300 ]; then
echo "✓ Simulation results posted to GitLab MR"
page=1
per_page=100
NOTE_ID=""
while true; do
GITLAB_NOTES_PAGE_FILE=$(mktemp)
http_notes=$(curl -sS -o "${GITLAB_NOTES_PAGE_FILE}" -w "%{http_code}" \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
"${API_URL}?per_page=${per_page}&page=${page}&sort=desc&order_by=created_at")
if [ "${http_notes}" != "200" ]; then
echo "Error: Failed to list GitLab MR notes (HTTP ${http_notes})"
rm -f "${GITLAB_NOTES_PAGE_FILE}"
GITLAB_NOTES_PAGE_FILE=""
exit 1
fi
MATCH_ID=$(jq -r \
--argjson uid "${GITLAB_USER_ID}" \
--arg marker "${OVERMIND_MARKER}" \
'[.[] | select(.system == false and .author.id == $uid and (.body | type == "string") and (.body | contains($marker)))] | if length > 0 then .[0].id | tostring else empty end' \
"${GITLAB_NOTES_PAGE_FILE}")
notes_count=$(jq 'length' "${GITLAB_NOTES_PAGE_FILE}")
rm -f "${GITLAB_NOTES_PAGE_FILE}"
GITLAB_NOTES_PAGE_FILE=""

if [ -n "${MATCH_ID}" ]; then
NOTE_ID="${MATCH_ID}"
break
fi
if [ "${notes_count}" -eq 0 ]; then
break
fi
if [ "${notes_count}" -lt "${per_page}" ]; then
break
fi
page=$((page + 1))
done

gitlab_post_json_body() {
_target_file="$1"
jq -n --rawfile body "${MARKDOWN_FILE}" '{body: $body}' > "${_target_file}"
}

gitlab_post_fallback_note() {
put_code="$1"
echo "Warning: Could not update GitLab MR note ${NOTE_ID} (HTTP ${put_code}); posting a new comment with an explanatory banner."
GITLAB_FALLBACK_BODY_FILE=$(mktemp)
{
printf '%s\n\n' "Overmind env0 plugin: could not update existing MR note (HTTP ${put_code}). A new comment was posted below; you may remove duplicate threads manually."
cat "${MARKDOWN_FILE}"
} > "${GITLAB_FALLBACK_BODY_FILE}"
JSON_PAYLOAD_FILE=$(mktemp)
jq -n --rawfile body "${GITLAB_FALLBACK_BODY_FILE}" '{body: $body}' > "${JSON_PAYLOAD_FILE}"
rm -f "${GITLAB_FALLBACK_BODY_FILE}"
GITLAB_FALLBACK_BODY_FILE=""
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" -X POST \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary "@${JSON_PAYLOAD_FILE}" \
"${API_URL}")
rm -f "${JSON_PAYLOAD_FILE}"
JSON_PAYLOAD_FILE=""
if [ "${HTTP_CODE}" -ge 200 ] && [ "${HTTP_CODE}" -lt 300 ]; then
echo "✓ Simulation results posted to GitLab MR (fallback after failed update)"
else
echo "Error: Failed to post fallback comment to GitLab MR (HTTP ${HTTP_CODE})"
exit 1
fi
}

if [ -n "${NOTE_ID}" ]; then
JSON_PAYLOAD_FILE=$(mktemp)
gitlab_post_json_body "${JSON_PAYLOAD_FILE}"
put_code=$(curl -sS -o /dev/null -w "%{http_code}" -X PUT \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary "@${JSON_PAYLOAD_FILE}" \
"${API_URL}/${NOTE_ID}")
rm -f "${JSON_PAYLOAD_FILE}"
JSON_PAYLOAD_FILE=""
if [ "${put_code}" -ge 200 ] && [ "${put_code}" -lt 300 ]; then
echo "✓ Simulation results updated on GitLab MR (note ${NOTE_ID})"
else
gitlab_post_fallback_note "${put_code}"
fi
else
echo "Error: Failed to post comment to GitLab MR (HTTP ${HTTP_CODE})"
exit 1
JSON_PAYLOAD_FILE=$(mktemp)
gitlab_post_json_body "${JSON_PAYLOAD_FILE}"
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" -X POST \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary "@${JSON_PAYLOAD_FILE}" \
"${API_URL}")
rm -f "${JSON_PAYLOAD_FILE}"
JSON_PAYLOAD_FILE=""
if [ "${HTTP_CODE}" -ge 200 ] && [ "${HTTP_CODE}" -lt 300 ]; then
echo "✓ Simulation results posted to GitLab MR"
else
echo "Error: Failed to post comment to GitLab MR (HTTP ${HTTP_CODE})"
exit 1
fi
fi
;;
github)
Expand Down
Loading