-
Notifications
You must be signed in to change notification settings - Fork 13
feat(#816): make retro agent multi-forge (GitHub + GitLab) #817
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
fullsend-ai-coder
wants to merge
1
commit into
main
Choose a base branch
from
agent/816-retro-multi-forge
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| version: 1 | ||
|
|
||
| # Sandbox policy for the retro agent (GitLab forge). | ||
| # | ||
| # Read-only agent: needs GitLab API (pipeline/job listing, issue search) | ||
| # and Vertex AI for inference. | ||
| # gh excluded from the binary allowlist — only curl is permitted for | ||
| # GitLab API access. Unlike GitHub (where curl is excluded as defense-in-depth), | ||
| # curl must be allowed because GitLab has no read-only CLI equivalent to gh. | ||
| # Write protection relies on the access:read-only enforcement on gitlab_api | ||
| # endpoints; the write-capable GITLAB_TOKEN is present in the sandbox for | ||
| # authentication but the policy restricts it to read-only API calls. | ||
|
|
||
| filesystem_policy: | ||
| include_workdir: true | ||
| read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] | ||
| read_write: [/sandbox, /tmp, /dev/null] | ||
| landlock: | ||
| compatibility: best_effort | ||
| process: | ||
| run_as_user: sandbox | ||
| run_as_group: sandbox | ||
|
|
||
| network_policies: | ||
| vertex_ai: | ||
| name: vertex-ai | ||
| endpoints: | ||
| - host: "api.anthropic.com" | ||
| port: 443 | ||
| protocol: rest | ||
| enforcement: enforce | ||
| access: read-write | ||
| - host: "*.googleapis.com" | ||
| port: 443 | ||
| protocol: rest | ||
| enforcement: enforce | ||
| access: read-write | ||
| binaries: | ||
| - path: "**/claude" | ||
| - path: "**/node" | ||
|
|
||
| gitlab_api: | ||
| name: gitlab-api | ||
| endpoints: | ||
| - host: "gitlab.com" | ||
| port: 443 | ||
| protocol: rest | ||
| enforcement: enforce | ||
| access: read-only | ||
| # Red Hat internal GitLab — supported deployment target for enrolled repos | ||
| - host: "gitlab.cee.redhat.com" | ||
| port: 443 | ||
| protocol: rest | ||
| enforcement: enforce | ||
| access: read-only | ||
| binaries: | ||
| - path: "**/curl" | ||
| - path: "**/node" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| #!/usr/bin/env bash | ||
| # shellcheck shell=bash | ||
| # github-retro-ops.lib.sh — GitHub forge operations for retro scripts. | ||
| # | ||
| # Bundled into pre-retro.sh and post-retro.sh via retro-ops.lib.sh. | ||
| # All functions use the gh CLI and the GitHub REST API. | ||
| # | ||
| # Expected globals (set by forge_parse_originating_url): | ||
| # ORIGINATING_REPO — owner/repo (e.g., "org/repo") | ||
| # ORIGINATING_NUMBER — issue or PR number | ||
| # | ||
| # Expected env vars: | ||
| # ORIGINATING_URL — HTML URL of the originating PR or issue | ||
| # GH_TOKEN — GitHub token with issues:write and pull_requests:write scope | ||
|
|
||
| [[ -n "${GITHUB_RETRO_OPS_SH_LOADED:-}" ]] && return 0 | ||
| GITHUB_RETRO_OPS_SH_LOADED=1 | ||
|
|
||
| # --- URL handling --- | ||
|
|
||
| forge_validate_originating_url() { | ||
| if [[ ! "${ORIGINATING_URL}" =~ ^https://github\.com/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+/(issues|pull)/[0-9]+$ ]]; then | ||
| echo "ERROR: ORIGINATING_URL does not match expected pattern: ${ORIGINATING_URL}" >&2 | ||
| return 1 | ||
| fi | ||
| } | ||
|
|
||
| forge_parse_originating_url() { | ||
| # shellcheck disable=SC2034 # ORIGINATING_REPO consumed by callers after function returns | ||
| ORIGINATING_REPO=$(echo "${ORIGINATING_URL}" | sed -E 's#https://github.com/##; s#/(issues|pull)/.*##') | ||
| # shellcheck disable=SC2034 # ORIGINATING_NUMBER consumed by callers after function returns | ||
| ORIGINATING_NUMBER=$(basename "${ORIGINATING_URL}") | ||
| } | ||
|
|
||
| # --- Token handling --- | ||
|
|
||
| forge_mask_token() { | ||
| echo "::add-mask::${GH_TOKEN}" | ||
| } | ||
|
|
||
| forge_require_token() { | ||
| : "${GH_TOKEN:?GH_TOKEN is required}" | ||
| } | ||
|
|
||
| # --- Config workspace --- | ||
|
|
||
| forge_get_config_workspace() { | ||
| echo "${GITHUB_WORKSPACE:-/tmp}" | ||
| } | ||
|
|
||
| # --- Comment limits --- | ||
|
|
||
| forge_get_comment_max_len() { | ||
| echo "65000" | ||
| } | ||
|
|
||
| # --- Labels --- | ||
|
|
||
| forge_create_label() { | ||
| local repo="$1" name="$2" description="$3" color="$4" | ||
| gh label create "${name}" --repo "${repo}" \ | ||
| --description "${description}" --color "${color}" \ | ||
| --force 2>/dev/null || true | ||
| } | ||
|
|
||
| # --- Issues --- | ||
|
|
||
| forge_create_issue() { | ||
| local repo="$1" title="$2" body="$3" label="$4" | ||
| gh issue create \ | ||
| --repo "${repo}" \ | ||
| --title "${title}" \ | ||
| --body "${body}" \ | ||
| --label "${label}" 2>&1 | ||
| } | ||
|
|
||
| # --- Comments --- | ||
|
|
||
| forge_post_comment() { | ||
| local repo="$1" number="$2" body="$3" | ||
| jq -nc --arg body "${body}" '{body: $body}' | gh api \ | ||
| "repos/${repo}/issues/${number}/comments" \ | ||
| --input - 2>&1 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| #!/usr/bin/env bash | ||
| # shellcheck shell=bash | ||
| # gitlab-retro-ops.lib.sh — GitLab forge operations for retro scripts. | ||
| # | ||
| # Bundled into pre-retro.sh and post-retro.sh via retro-ops.lib.sh. | ||
| # All functions use curl against the GitLab REST API. | ||
| # | ||
| # Expected globals (set by forge_parse_originating_url): | ||
| # GITLAB_HOST — API host (e.g., "gitlab.com") | ||
| # ORIGINATING_REPO — plain project path (e.g., "group/project") | ||
| # ORIGINATING_REPO_ENCODED — URL-encoded project path | ||
| # ORIGINATING_NUMBER — issue IID or MR IID | ||
| # ORIGINATING_RESOURCE — "issues" or "merge_requests" | ||
| # | ||
| # Expected env vars: | ||
| # ORIGINATING_URL — HTML URL of the originating MR or issue | ||
| # GITLAB_TOKEN — GitLab personal/project access token | ||
|
|
||
| [[ -n "${GITLAB_RETRO_OPS_SH_LOADED:-}" ]] && return 0 | ||
| GITLAB_RETRO_OPS_SH_LOADED=1 | ||
|
|
||
| _gitlab_api() { | ||
|
ggallen marked this conversation as resolved.
ggallen marked this conversation as resolved.
|
||
| local method="$1" | ||
| shift | ||
| local endpoint="$1" | ||
| shift | ||
| curl --fail --silent --show-error \ | ||
| --connect-timeout 10 --max-time 30 \ | ||
| --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \ | ||
| --request "${method}" \ | ||
| "https://${GITLAB_HOST}/api/v4${endpoint}" \ | ||
| "$@" | ||
| } | ||
|
|
||
| # --- URL handling --- | ||
|
|
||
| forge_validate_originating_url() { | ||
| # Accept both issue and MR URLs: /-/issues/N or /-/merge_requests/N | ||
| if [[ ! "${ORIGINATING_URL}" =~ ^https://[a-zA-Z0-9._-]+(/[a-zA-Z0-9._-]+)+/-/(issues|merge_requests)/[0-9]+$ ]]; then | ||
|
ggallen marked this conversation as resolved.
ggallen marked this conversation as resolved.
|
||
| echo "ERROR: ORIGINATING_URL does not match expected GitLab pattern: ${ORIGINATING_URL}" >&2 | ||
| return 1 | ||
|
ggallen marked this conversation as resolved.
|
||
| fi | ||
| local host | ||
| host=$(echo "${ORIGINATING_URL}" | sed -E 's#^https://([^/]+)/.*#\1#') | ||
| case "${host}" in | ||
| gitlab.com|gitlab.cee.redhat.com) ;; | ||
| *) echo "ERROR: GitLab host '${host}' is not in the allowed host list" >&2; return 1 ;; | ||
| esac | ||
| } | ||
|
|
||
| forge_parse_originating_url() { | ||
| # Extract host, project path, resource type, and number from URL. | ||
| # e.g., https://gitlab.com/group/subgroup/project/-/issues/42 | ||
| # e.g., https://gitlab.com/group/project/-/merge_requests/10 | ||
| # shellcheck disable=SC2034 # GITLAB_HOST consumed by _gitlab_api and callers | ||
| GITLAB_HOST=$(echo "${ORIGINATING_URL}" | sed -E 's#^https://([^/]+)/.*#\1#') | ||
| ORIGINATING_REPO=$(echo "${ORIGINATING_URL}" | sed -E 's#^https://[^/]+/(.+)/-/(issues|merge_requests)/[0-9]+$#\1#') | ||
| # shellcheck disable=SC2034 # ORIGINATING_REPO_ENCODED consumed by _gitlab_api calls | ||
| ORIGINATING_REPO_ENCODED=$(printf '%s' "${ORIGINATING_REPO}" | jq -sRr @uri) | ||
| # shellcheck disable=SC2034 # ORIGINATING_NUMBER consumed by callers after function returns | ||
| ORIGINATING_NUMBER=$(basename "${ORIGINATING_URL}") | ||
| # Detect resource type: issues or merge_requests | ||
| if [[ "${ORIGINATING_URL}" == *"/-/merge_requests/"* ]]; then | ||
| # shellcheck disable=SC2034 # ORIGINATING_RESOURCE consumed by forge_post_comment | ||
| ORIGINATING_RESOURCE="merge_requests" | ||
| else | ||
| # shellcheck disable=SC2034 # ORIGINATING_RESOURCE consumed by forge_post_comment | ||
| ORIGINATING_RESOURCE="issues" | ||
| fi | ||
| } | ||
|
|
||
| # --- Token handling --- | ||
|
|
||
| forge_mask_token() { | ||
|
ggallen marked this conversation as resolved.
ggallen marked this conversation as resolved.
|
||
| # ::add-mask:: is GHA-only; on non-GHA runners the echo would leak the token. | ||
| if [[ -n "${GITHUB_ACTIONS:-}" ]]; then | ||
| echo "::add-mask::${GITLAB_TOKEN}" | ||
| fi | ||
| } | ||
|
|
||
| forge_require_token() { | ||
| : "${GITLAB_TOKEN:?GITLAB_TOKEN is required}" | ||
| } | ||
|
|
||
| # --- Config workspace --- | ||
|
|
||
| forge_get_config_workspace() { | ||
| echo "${CI_PROJECT_DIR:-/tmp}" | ||
| } | ||
|
|
||
| # --- Comment limits --- | ||
|
|
||
| forge_get_comment_max_len() { | ||
| echo "1000000" | ||
| } | ||
|
|
||
| # --- Labels --- | ||
|
|
||
| forge_create_label() { | ||
| local repo="$1" name="$2" description="$3" color="$4" | ||
| local repo_encoded | ||
| repo_encoded=$(printf '%s' "${repo}" | jq -sRr @uri) | ||
| _gitlab_api POST "/projects/${repo_encoded}/labels" \ | ||
|
ggallen marked this conversation as resolved.
|
||
| --data-urlencode "name=${name}" \ | ||
| --data-urlencode "description=${description}" \ | ||
| --data-urlencode "color=#${color}" > /dev/null 2>/dev/null || true | ||
| } | ||
|
|
||
| # --- Issues --- | ||
|
|
||
|
ggallen marked this conversation as resolved.
|
||
| forge_create_issue() { | ||
| local repo="$1" title="$2" body="$3" label="$4" | ||
| local repo_encoded | ||
| repo_encoded=$(printf '%s' "${repo}" | jq -sRr @uri) | ||
| local response | ||
| response=$(_gitlab_api POST "/projects/${repo_encoded}/issues" \ | ||
| --data-urlencode "title=${title}" \ | ||
| --data-urlencode "description=${body}" \ | ||
| --data-urlencode "labels=${label}" 2>&1) || { | ||
| echo "GitLab API error: failed to create issue in ${repo}: ${response}" | ||
| return 1 | ||
| } | ||
| local url | ||
| url=$(echo "${response}" | jq -r '.web_url') | ||
| if [[ -z "${url}" || "${url}" == "null" ]]; then | ||
| echo "GitLab API error: unexpected response from issue creation in ${repo}" | ||
| return 1 | ||
| fi | ||
| echo "${url}" | ||
| } | ||
|
|
||
| # --- Comments --- | ||
|
|
||
| forge_post_comment() { | ||
| local repo="$1" number="$2" body="$3" | ||
| local repo_encoded | ||
| repo_encoded=$(printf '%s' "${repo}" | jq -sRr @uri) | ||
| _gitlab_api POST "/projects/${repo_encoded}/${ORIGINATING_RESOURCE}/${number}/notes" \ | ||
|
ggallen marked this conversation as resolved.
ggallen marked this conversation as resolved.
ggallen marked this conversation as resolved.
|
||
| --data-urlencode "body=${body}" 2>&1 | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.