@openhands/extensions 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.release-please-manifest.json +1 -1
- package/README.md +3 -2
- package/automations/catalog/github-pr-reviewer.json +1 -1
- package/automations/catalog/github-repo-monitor.json +1 -1
- package/marketplaces/openhands-extensions.json +13 -0
- package/package.json +1 -1
- package/plugins/issue-duplicate-checker/action.yml +2 -2
- package/plugins/pr-review/README.md +1 -1
- package/plugins/qa-changes/action.yml +4 -2
- package/plugins/release-notes/action.yml +3 -2
- package/plugins/release-notes/workflows/release-notes.yml +10 -4
- package/plugins/vulnerability-remediation/README.md +4 -2
- package/plugins/vulnerability-remediation/action.yml +8 -4
- package/pyproject.toml +1 -1
- package/scripts/sync_extensions.py +2 -1
- package/skills/github-actions/.plugin/plugin.json +19 -0
- package/skills/github-actions/README.md +190 -0
- package/skills/github-actions/SKILL.md +44 -0
- package/skills/github-repo-monitor/README.md +1 -1
- package/skills/github-repo-monitor/SKILL.md +9 -9
- package/skills/github-repo-monitor/references/github-api.md +3 -3
- package/skills/github-repo-monitor/scripts/main.py +11 -11
- package/skills/index.js +14 -2
- package/skills/slack-channel-monitor/README.md +2 -1
- package/skills/slack-channel-monitor/SKILL.md +22 -4
- package/tests/test_sync_extensions.py +14 -0
- /package/plugins/issue-duplicate-checker/scripts/{post_duplicate_notice.js → post_duplicate_notice.cjs} +0 -0
- /package/plugins/issue-duplicate-checker/scripts/{remove_duplicate_candidate_label.js → remove_duplicate_candidate_label.cjs} +0 -0
- /package/skills/github-pr-reviewer/commands/{pr-reviewer:setup.md → pr-reviewer-setup.md} +0 -0
- /package/skills/github-repo-monitor/commands/{github-monitor:poll.md → github-monitor-poll.md} +0 -0
- /package/skills/incident-retrospective/commands/{incident-retro:setup.md → incident-retro-setup.md} +0 -0
- /package/skills/linear-triage/commands/{linear-triage:setup.md → linear-triage-setup.md} +0 -0
- /package/skills/openhands-automation/commands/{automation:create.md → automation-create.md} +0 -0
- /package/skills/research-brief/commands/{research-brief:setup.md → research-brief-setup.md} +0 -0
- /package/skills/slack-channel-monitor/commands/{slack-monitor:poll.md → slack-monitor-poll.md} +0 -0
- /package/skills/slack-standup-digest/commands/{standup-digest:setup.md → standup-digest-setup.md} +0 -0
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ See [`integrations/README.md`](integrations/README.md), [`automations/README.md`
|
|
|
60
60
|
## Extensions Catalog
|
|
61
61
|
|
|
62
62
|
<!-- BEGIN AUTO-GENERATED CATALOG -->
|
|
63
|
-
This repository contains **2 marketplace(s)** with **
|
|
63
|
+
This repository contains **2 marketplace(s)** with **57 extensions** (47 skills, 10 plugins).
|
|
64
64
|
|
|
65
65
|
### large-codebase
|
|
66
66
|
|
|
@@ -79,7 +79,7 @@ OpenHands skills for interacting, improving, and refactoring large codebases
|
|
|
79
79
|
|
|
80
80
|
Official skills and plugins for OpenHands — the open-source AI software engineer.
|
|
81
81
|
|
|
82
|
-
**
|
|
82
|
+
**53 extensions** (45 skills, 8 plugins)
|
|
83
83
|
|
|
84
84
|
| Name | Type | Description | Commands |
|
|
85
85
|
|------|------|-------------|----------|
|
|
@@ -100,6 +100,7 @@ Official skills and plugins for OpenHands — the open-source AI software engine
|
|
|
100
100
|
| flarglebargle | skill | A test skill that responds to the magic word 'flarglebargle' with a compliment. Use for testing skill activation and ... | — |
|
|
101
101
|
| frontend-design | skill | Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks ... | — |
|
|
102
102
|
| github | skill | Interact with GitHub repositories, pull requests, issues, and workflows using the GITHUB_TOKEN environment variable a... | — |
|
|
103
|
+
| github-actions | skill | Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating wo... | — |
|
|
103
104
|
| github-pr-review | skill | Post structured PR reviews to GitHub with inline comments/suggestions in a single API call. | `/github-pr-review` |
|
|
104
105
|
| github-pr-reviewer | skill | Create an automation that reviews GitHub pull requests when they are opened or updated. Inspects the diff, changed fi... | `/pr-reviewer:setup` |
|
|
105
106
|
| github-repo-monitor | skill | Create a cron automation that polls a GitHub repository for issue and PR comments containing a configurable trigger p... | `/github-monitor:poll` |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "github-pr-reviewer",
|
|
3
|
-
"name": "GitHub
|
|
3
|
+
"name": "GitHub Code Review Agent",
|
|
4
4
|
"category": "Code review",
|
|
5
5
|
"description": "Watch pull requests, inspect the diff, and leave a concise review with risks and suggested follow-ups.",
|
|
6
6
|
"requiredIntegrationIds": [
|
|
@@ -7,5 +7,5 @@
|
|
|
7
7
|
"popularityRank": 98,
|
|
8
8
|
"estimatedSetupMinutes": 5,
|
|
9
9
|
"prompt": "/github-monitor:poll",
|
|
10
|
-
"exampleImplementation": "Trigger: cron, every minute (configurable)\nRequired secret:
|
|
10
|
+
"exampleImplementation": "Trigger: cron, every minute (configurable)\nRequired secret: GITHUB_PERSONAL_ACCESS_TOKEN\n\n1. Poll GitHub for new issue and PR comments since the last run.\n2. Match comments containing the trigger phrase (case-insensitive, default: @OpenHands).\n3. Post an acknowledgment comment with a link to the new OpenHands conversation.\n4. Forward follow-up replies in the same thread to the running conversation.\n5. Post the agent's final response back to GitHub when the conversation completes."
|
|
11
11
|
}
|
|
@@ -238,6 +238,19 @@
|
|
|
238
238
|
"workflows"
|
|
239
239
|
]
|
|
240
240
|
},
|
|
241
|
+
{
|
|
242
|
+
"name": "github-actions",
|
|
243
|
+
"source": "./skills/github-actions",
|
|
244
|
+
"description": "Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating workflows, or troubleshooting GitHub Actions.",
|
|
245
|
+
"category": "integration",
|
|
246
|
+
"keywords": [
|
|
247
|
+
"github-actions",
|
|
248
|
+
"workflows",
|
|
249
|
+
"ci-cd",
|
|
250
|
+
"automation",
|
|
251
|
+
"actions"
|
|
252
|
+
]
|
|
253
|
+
},
|
|
241
254
|
{
|
|
242
255
|
"name": "github-pr-review",
|
|
243
256
|
"source": "./skills/github-pr-review",
|
package/package.json
CHANGED
|
@@ -258,7 +258,7 @@ runs:
|
|
|
258
258
|
if: inputs.mode == 'issue-check' && steps.parsed_result.outputs.should_comment == 'true'
|
|
259
259
|
uses: actions/github-script@v9
|
|
260
260
|
env:
|
|
261
|
-
ACTION_SCRIPT: ${{ github.action_path }}/scripts/post_duplicate_notice.
|
|
261
|
+
ACTION_SCRIPT: ${{ github.action_path }}/scripts/post_duplicate_notice.cjs
|
|
262
262
|
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
263
263
|
SUMMARY: ${{ steps.parsed_result.outputs.summary }}
|
|
264
264
|
CANDIDATE_ISSUES_JSON: ${{ steps.parsed_result.outputs.candidate_issues_json }}
|
|
@@ -341,7 +341,7 @@ runs:
|
|
|
341
341
|
if: inputs.mode == 'remove-label'
|
|
342
342
|
uses: actions/github-script@v9
|
|
343
343
|
env:
|
|
344
|
-
ACTION_SCRIPT: ${{ github.action_path }}/scripts/remove_duplicate_candidate_label.
|
|
344
|
+
ACTION_SCRIPT: ${{ github.action_path }}/scripts/remove_duplicate_candidate_label.cjs
|
|
345
345
|
with:
|
|
346
346
|
github-token: ${{ inputs.github-token }}
|
|
347
347
|
script: |
|
|
@@ -69,7 +69,7 @@ Add the following secrets in your repository settings (**Settings → Secrets an
|
|
|
69
69
|
| `GITHUB_TOKEN` | Auto | Provided automatically by GitHub Actions |
|
|
70
70
|
| `LMNR_SKILLS_API_KEY` | No | Laminar API key (org-level secret; mapped to `LMNR_PROJECT_API_KEY` env var in workflows) |
|
|
71
71
|
|
|
72
|
-
**Note**:
|
|
72
|
+
**Note**: To post review comments from a bot account instead of `GITHUB_TOKEN`, use a bot token scoped to the repository's trust boundary. On **public** repositories, use `OPENHANDS_BOT_GITHUB_PAT_PUBLIC` — a fine-grained token limited to public repositories. On **private** repositories, use a bot token scoped to that repository. Never give a public repository a token that can reach private repositories.
|
|
73
73
|
|
|
74
74
|
### 3. Customize the Workflow (Optional)
|
|
75
75
|
|
|
@@ -119,6 +119,8 @@ runs:
|
|
|
119
119
|
env:
|
|
120
120
|
LLM_API_KEY: ${{ inputs.llm-api-key }}
|
|
121
121
|
GITHUB_TOKEN: ${{ inputs.github-token }}
|
|
122
|
+
GITHUB_EVENT_PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }}
|
|
123
|
+
INPUTS_LLM_MODEL: ${{ inputs.llm-model }}
|
|
122
124
|
run: |
|
|
123
125
|
if [ -z "$LLM_API_KEY" ]; then
|
|
124
126
|
echo "Error: llm-api-key is required."
|
|
@@ -130,9 +132,9 @@ runs:
|
|
|
130
132
|
fi
|
|
131
133
|
|
|
132
134
|
echo "PR Number: ${{ github.event.pull_request.number }}"
|
|
133
|
-
echo "PR Title: $
|
|
135
|
+
echo "PR Title: $GITHUB_EVENT_PULL_REQUEST_TITLE"
|
|
134
136
|
echo "Repository: ${{ github.repository }}"
|
|
135
|
-
echo "LLM model: $
|
|
137
|
+
echo "LLM model: $INPUTS_LLM_MODEL"
|
|
136
138
|
|
|
137
139
|
- name: Run QA validation
|
|
138
140
|
if: steps.preflight.outputs.skip != 'true'
|
|
@@ -109,9 +109,10 @@ runs:
|
|
|
109
109
|
shell: bash
|
|
110
110
|
env:
|
|
111
111
|
GITHUB_TOKEN: ${{ inputs.github-token }}
|
|
112
|
+
INPUTS_TAG: ${{ inputs.tag }}
|
|
112
113
|
run: |
|
|
113
114
|
if [ -f release_notes.md ]; then
|
|
114
|
-
echo "Updating release notes for tag $
|
|
115
|
-
gh release edit "$
|
|
115
|
+
echo "Updating release notes for tag $INPUTS_TAG"
|
|
116
|
+
gh release edit "$INPUTS_TAG" --notes-file release_notes.md || \
|
|
116
117
|
echo "Note: Could not update release. Release may not exist yet."
|
|
117
118
|
fi
|
|
@@ -38,11 +38,14 @@ jobs:
|
|
|
38
38
|
|
|
39
39
|
- name: Determine tag
|
|
40
40
|
id: get-tag
|
|
41
|
+
env:
|
|
42
|
+
GITHUB_EVENT_INPUTS_TAG: ${{ github.event.inputs.tag }}
|
|
43
|
+
GITHUB_REF_NAME: ${{ github.ref_name }}
|
|
41
44
|
run: |
|
|
42
45
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
43
|
-
echo "tag=$
|
|
46
|
+
echo "tag=$GITHUB_EVENT_INPUTS_TAG" >> $GITHUB_OUTPUT
|
|
44
47
|
else
|
|
45
|
-
echo "tag=$
|
|
48
|
+
echo "tag=$GITHUB_REF_NAME" >> $GITHUB_OUTPUT
|
|
46
49
|
fi
|
|
47
50
|
|
|
48
51
|
- name: Generate Release Notes
|
|
@@ -57,10 +60,13 @@ jobs:
|
|
|
57
60
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
58
61
|
|
|
59
62
|
- name: Display generated notes
|
|
63
|
+
env:
|
|
64
|
+
TAG: ${{ steps.get-tag.outputs.tag }}
|
|
65
|
+
PREVIOUS_TAG: ${{ steps.release-notes.outputs.previous-tag }}
|
|
60
66
|
run: |
|
|
61
|
-
echo "## Release Notes for $
|
|
67
|
+
echo "## Release Notes for $TAG"
|
|
62
68
|
echo ""
|
|
63
|
-
echo "Previous tag: $
|
|
69
|
+
echo "Previous tag: $PREVIOUS_TAG"
|
|
64
70
|
echo "Commits: ${{ steps.release-notes.outputs.commit-count }}"
|
|
65
71
|
echo "Contributors: ${{ steps.release-notes.outputs.contributor-count }}"
|
|
66
72
|
echo "New contributors: ${{ steps.release-notes.outputs.new-contributor-count }}"
|
|
@@ -90,12 +90,14 @@ on:
|
|
|
90
90
|
|
|
91
91
|
### Optional: Use a Bot Account
|
|
92
92
|
|
|
93
|
-
For better PR attribution, use a bot PAT:
|
|
93
|
+
For better PR attribution, use a bot PAT scoped to the repository's trust boundary. On **public** repositories, use the public-scoped token:
|
|
94
94
|
|
|
95
95
|
```yaml
|
|
96
|
-
github-token: ${{ secrets.
|
|
96
|
+
github-token: ${{ secrets.OPENHANDS_BOT_GITHUB_PAT_PUBLIC || secrets.GITHUB_TOKEN }}
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
On **private** repositories, use a bot token scoped to that repository instead. Never give a public repository a token that can reach private repositories.
|
|
100
|
+
|
|
99
101
|
## Usage
|
|
100
102
|
|
|
101
103
|
### Automatic Scheduled Scans
|
|
@@ -96,6 +96,10 @@ runs:
|
|
|
96
96
|
env:
|
|
97
97
|
LLM_API_KEY: ${{ inputs.llm-api-key }}
|
|
98
98
|
GITHUB_TOKEN: ${{ inputs.github-token }}
|
|
99
|
+
INPUTS_EXTENSIONS_VERSION: ${{ inputs.extensions-version }}
|
|
100
|
+
INPUTS_LLM_MODEL: ${{ inputs.llm-model }}
|
|
101
|
+
INPUTS_SEVERITY_THRESHOLD: ${{ inputs.severity-threshold }}
|
|
102
|
+
INPUTS_MAX_VULNERABILITIES: ${{ inputs.max-vulnerabilities }}
|
|
99
103
|
run: |
|
|
100
104
|
if [ -z "$LLM_API_KEY" ]; then
|
|
101
105
|
echo "Error: llm-api-key is required."
|
|
@@ -108,10 +112,10 @@ runs:
|
|
|
108
112
|
fi
|
|
109
113
|
|
|
110
114
|
echo "Repository: ${{ github.repository }}"
|
|
111
|
-
echo "Extensions Version: $
|
|
112
|
-
echo "LLM Model: $
|
|
113
|
-
echo "Severity Threshold: $
|
|
114
|
-
echo "Max Vulnerabilities: $
|
|
115
|
+
echo "Extensions Version: $INPUTS_EXTENSIONS_VERSION"
|
|
116
|
+
echo "LLM Model: $INPUTS_LLM_MODEL"
|
|
117
|
+
echo "Severity Threshold: $INPUTS_SEVERITY_THRESHOLD"
|
|
118
|
+
echo "Max Vulnerabilities: $INPUTS_MAX_VULNERABILITIES"
|
|
115
119
|
|
|
116
120
|
- name: Run vulnerability scan
|
|
117
121
|
id: scan
|
package/pyproject.toml
CHANGED
|
@@ -143,7 +143,8 @@ def collect_needed_commands() -> list[CommandSpec]:
|
|
|
143
143
|
meta = parse_frontmatter(skill_md.read_text())
|
|
144
144
|
desc = str(meta.get("description", ""))
|
|
145
145
|
for trigger in slash_triggers(meta):
|
|
146
|
-
|
|
146
|
+
# Replace colons with dashes for cross-platform filename compatibility
|
|
147
|
+
cmd_name = trigger.lstrip("/").replace(":", "-")
|
|
147
148
|
cmd_path = skill_dir / "commands" / f"{cmd_name}.md"
|
|
148
149
|
needed.append(CommandSpec(path=cmd_path, trigger=trigger, description=desc))
|
|
149
150
|
return needed
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "github-actions",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating workflows, or troubleshooting GitHub Actions.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "OpenHands",
|
|
7
|
+
"email": "contact@all-hands.dev"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/OpenHands/extensions",
|
|
10
|
+
"repository": "https://github.com/OpenHands/extensions",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"github-actions",
|
|
14
|
+
"workflows",
|
|
15
|
+
"ci-cd",
|
|
16
|
+
"automation",
|
|
17
|
+
"actions"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# GitHub Actions Skill
|
|
2
|
+
|
|
3
|
+
A skill focused on **effectiveness** when working with GitHub Actions — monitoring runs, reading logs, and developing actions with confidence rather than guessing.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This skill provides guidance for:
|
|
8
|
+
- Creating and structuring workflows
|
|
9
|
+
- Building custom composite and reusable actions
|
|
10
|
+
- Testing actions locally (before burning CI minutes) and in CI
|
|
11
|
+
- Debugging failed workflows from real evidence (logs), not assumptions
|
|
12
|
+
- Avoiding common pitfalls around permissions, secrets, and fork PRs
|
|
13
|
+
|
|
14
|
+
## When to Use This Skill
|
|
15
|
+
|
|
16
|
+
- Creating new GitHub Actions workflows
|
|
17
|
+
- Building custom composite or reusable actions
|
|
18
|
+
- Debugging workflow failures
|
|
19
|
+
- Setting up CI/CD pipelines
|
|
20
|
+
- Troubleshooting permission or secret issues
|
|
21
|
+
- Testing actions before merging to main
|
|
22
|
+
|
|
23
|
+
## Cost Awareness
|
|
24
|
+
|
|
25
|
+
Every workflow run consumes CI minutes (billed minutes on private repos, shared capacity on public ones).
|
|
26
|
+
Plan before you push:
|
|
27
|
+
|
|
28
|
+
- Prefer one targeted commit that exercises the change over multiple speculative pushes
|
|
29
|
+
- Use `workflow_dispatch` with inputs to manually trigger only what you need
|
|
30
|
+
- Use `paths:` filters and `if:` conditions so jobs only run when relevant
|
|
31
|
+
- Run with `act` locally before opening a PR when feasible
|
|
32
|
+
- Cancel obsolete runs with `concurrency:` groups (don't pay for runs you've already invalidated)
|
|
33
|
+
- Use the smallest matrix that proves the change works; expand only if needed
|
|
34
|
+
|
|
35
|
+
## Develop With Confidence: The Loop
|
|
36
|
+
|
|
37
|
+
Don't change → push → hope. Use this loop:
|
|
38
|
+
|
|
39
|
+
1. **Read** the existing workflow / action carefully. Note triggers, permissions, inputs, env, and secrets.
|
|
40
|
+
2. **Reproduce locally** with `act` when possible (see below).
|
|
41
|
+
3. **Add visibility** — debug steps that print non-secret inputs, refs, and intermediate state.
|
|
42
|
+
4. **Push a single focused commit**, then watch the run live.
|
|
43
|
+
5. **Read the full failed-job log** (not just the summary) before editing.
|
|
44
|
+
6. **Form a hypothesis from the log**, change the smallest thing that tests it, push, watch again.
|
|
45
|
+
|
|
46
|
+
## Monitoring Runs With `gh`
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Watch the most recent run from the current branch in real time
|
|
50
|
+
gh run watch
|
|
51
|
+
|
|
52
|
+
# Watch a specific run
|
|
53
|
+
gh run watch <run-id>
|
|
54
|
+
|
|
55
|
+
# Auto-refresh PR checks until they finish (every 10s)
|
|
56
|
+
gh pr checks <pr-number> --watch --interval 10
|
|
57
|
+
|
|
58
|
+
# List recent runs for the repo / branch
|
|
59
|
+
gh run list --branch <branch> --limit 10
|
|
60
|
+
|
|
61
|
+
# Get the run ID for the latest run on a branch
|
|
62
|
+
gh run list --branch <branch> --limit 1 --json databaseId --jq '.[0].databaseId'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Reading Logs (Don't Guess)
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Full log for a run
|
|
69
|
+
gh run view <run-id> --log
|
|
70
|
+
|
|
71
|
+
# Just the failed steps (much shorter)
|
|
72
|
+
gh run view <run-id> --log-failed
|
|
73
|
+
|
|
74
|
+
# Log for a specific job (useful for matrix builds)
|
|
75
|
+
gh run view <run-id> --log --job <job-id>
|
|
76
|
+
|
|
77
|
+
# List jobs for a run to find the job ID
|
|
78
|
+
gh run view <run-id> --json jobs --jq '.jobs[] | {id, name, conclusion}'
|
|
79
|
+
|
|
80
|
+
# Rerun only the failed jobs (avoid paying to rerun green ones)
|
|
81
|
+
gh run rerun <run-id> --failed
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
When a step fails, look for:
|
|
85
|
+
- The exact command that exited non-zero (above the `##[error]` line)
|
|
86
|
+
- The shell — bash on Linux, `pwsh` on Windows runners — error messages differ
|
|
87
|
+
- Values printed by your debug step; compare against what the workflow *thought* it was passing in
|
|
88
|
+
|
|
89
|
+
## Adding Visibility to Actions
|
|
90
|
+
|
|
91
|
+
When creating a new action or hitting a tricky bug, add debug steps. Don't leave them in indefinitely — remove or guard them with `if: runner.debug == '1'` once the issue is understood.
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
steps:
|
|
95
|
+
# Print non-secret inputs and context at the start
|
|
96
|
+
- name: Debug - inputs and context
|
|
97
|
+
if: runner.debug == '1' || inputs.debug == 'true'
|
|
98
|
+
run: |
|
|
99
|
+
echo "Event: ${{ github.event_name }}"
|
|
100
|
+
echo "Ref: ${{ github.ref }}"
|
|
101
|
+
echo "SHA: ${{ github.sha }}"
|
|
102
|
+
echo "Actor: ${{ github.actor }}"
|
|
103
|
+
echo "PWD: $(pwd)"
|
|
104
|
+
echo "Input.my-param: ${{ inputs.my-param }}"
|
|
105
|
+
echo "Files:"
|
|
106
|
+
ls -la
|
|
107
|
+
|
|
108
|
+
# ... your real steps ...
|
|
109
|
+
|
|
110
|
+
# Verify outcome before the job ends
|
|
111
|
+
- name: Debug - verify outputs
|
|
112
|
+
if: always() && (runner.debug == '1' || inputs.debug == 'true')
|
|
113
|
+
run: |
|
|
114
|
+
echo "Generated files:"
|
|
115
|
+
ls -la dist/ || echo "dist/ not found"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
To enable `runner.debug == '1'` for a single run, re-run the workflow with **"Enable debug logging"** checked, or set the repo secret `ACTIONS_RUNNER_DEBUG=true`.
|
|
119
|
+
|
|
120
|
+
**Never** echo `${{ secrets.* }}` — GitHub masks them, but encoded forms (base64, hex, JSON-wrapped) can leak through.
|
|
121
|
+
|
|
122
|
+
## Testing Locally With `act`
|
|
123
|
+
|
|
124
|
+
`act` ([nektos/act](https://github.com/nektos/act)) runs workflows in Docker locally (Docker must be installed and running). Use it to iterate without burning CI minutes.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Run the default push event
|
|
128
|
+
act
|
|
129
|
+
|
|
130
|
+
# Simulate a pull_request event
|
|
131
|
+
act pull_request
|
|
132
|
+
|
|
133
|
+
# Run a specific job
|
|
134
|
+
act -j build
|
|
135
|
+
|
|
136
|
+
# Pass secrets
|
|
137
|
+
act -s GITHUB_TOKEN="$GITHUB_TOKEN"
|
|
138
|
+
|
|
139
|
+
# Use a larger image closer to GitHub's runner
|
|
140
|
+
act -P ubuntu-latest=catthehacker/ubuntu:full-latest
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Caveats: `act` does not perfectly emulate GitHub's runners. Things that may differ: pre-installed tools, `GITHUB_TOKEN` permissions, OIDC, the `secrets.GITHUB_TOKEN` value, and macOS/Windows runners. Treat green-on-`act` as a strong signal, not a guarantee.
|
|
144
|
+
|
|
145
|
+
## Testing Custom Actions Requires Merge to Main
|
|
146
|
+
|
|
147
|
+
A repository-local custom action referenced as `uses: ./.github/actions/my-action` works from any branch, but:
|
|
148
|
+
|
|
149
|
+
- A custom action published as `uses: owner/repo/action@ref` must have the action's files **on `ref`** to be resolvable
|
|
150
|
+
- `@main` or `@v1` must already exist when the workflow that consumes it runs
|
|
151
|
+
- This is why a brand-new action typically must land on `main` first, then be consumed from feature branches that reference `@main` (or a tag)
|
|
152
|
+
|
|
153
|
+
If you need to iterate on an action and its consumer together, point `uses:` at a SHA on your branch, or develop the action via the local `./.github/actions/…` path until it's stable.
|
|
154
|
+
|
|
155
|
+
## Common Pitfalls
|
|
156
|
+
|
|
157
|
+
1. **Custom action not found from a PR** — the action's ref doesn't exist yet. Merge to `main` (or push a tag), then consume.
|
|
158
|
+
2. **`GITHUB_TOKEN` permissions** — defaults can be read-only. Set `permissions:` explicitly at the workflow or job level.
|
|
159
|
+
3. **`pull_request` vs `pull_request_target`** — `pull_request` runs in the fork's context (no secrets); `pull_request_target` runs with repo secrets against the base — **dangerous** if you check out PR code and execute it. Don't.
|
|
160
|
+
4. **Secrets in fork PRs** — even non-`*_target` workflows hide secrets from forks. Don't design workflows that require secrets to run on community PRs.
|
|
161
|
+
5. **`schedule:` triggers only fire from the default branch** — a `schedule:` trigger added in a PR only fires after it lands on the default branch; `workflow_dispatch` similarly won't appear in the Actions tab UI until merged to default (though it can be triggered via API on any branch); `pull_request`-triggered workflows do run on the PR that introduces them.
|
|
162
|
+
6. **Path filters are OR, not AND** — any matching path triggers; you can't require all paths.
|
|
163
|
+
7. **Matrix `fail-fast: true`** (default) cancels other matrix legs on first failure — turn off when you need full coverage.
|
|
164
|
+
8. **Artifacts** — files don't persist between jobs unless uploaded via `actions/upload-artifact` and downloaded in the consuming job.
|
|
165
|
+
9. **`env:` scope** — env at workflow, job, and step levels all exist. Step-level shadows job-level shadows workflow-level. Surprises follow if you forget.
|
|
166
|
+
10. **Pin versions** — use `@v4` (or better, a SHA) rather than `@main`. Floating refs break builds when upstream changes.
|
|
167
|
+
|
|
168
|
+
## Best Practices
|
|
169
|
+
|
|
170
|
+
1. **Pin action versions** — `@v4` or SHA, not `@main`
|
|
171
|
+
2. **Least privilege** — set `permissions:` to the minimum needed
|
|
172
|
+
3. **Debug steps when introducing or debugging** — remove or gate them with `runner.debug` afterward
|
|
173
|
+
4. **Cache dependencies** — `actions/cache` saves real minutes
|
|
174
|
+
5. **Concurrency** — cancel obsolete runs:
|
|
175
|
+
```yaml
|
|
176
|
+
concurrency:
|
|
177
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
178
|
+
cancel-in-progress: true
|
|
179
|
+
```
|
|
180
|
+
6. **Timeouts** — set `timeout-minutes:` on jobs that can hang
|
|
181
|
+
7. **Fail loudly** — `set -euo pipefail` in non-trivial bash steps
|
|
182
|
+
8. **Document non-obvious triggers/inputs** at the top of the workflow file
|
|
183
|
+
|
|
184
|
+
## Resources
|
|
185
|
+
|
|
186
|
+
- [GitHub Actions documentation](https://docs.github.com/en/actions)
|
|
187
|
+
- [`act` — run actions locally](https://github.com/nektos/act)
|
|
188
|
+
- [`gh run` CLI reference](https://cli.github.com/manual/gh_run)
|
|
189
|
+
- [github skill](https://github.com/OpenHands/extensions/tree/main/skills/github) — GitHub API, PRs, issues, and repos
|
|
190
|
+
- [github-pr-review skill](https://github.com/OpenHands/extensions/tree/main/skills/github-pr-review) — post structured code reviews via the GitHub API
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: github-actions
|
|
3
|
+
description: Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating workflows, or troubleshooting GitHub Actions.
|
|
4
|
+
triggers:
|
|
5
|
+
- github actions
|
|
6
|
+
- github workflow
|
|
7
|
+
- actions workflow
|
|
8
|
+
- gh actions
|
|
9
|
+
- .github/workflows
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# GitHub Actions Guide
|
|
13
|
+
|
|
14
|
+
## Critical Rules
|
|
15
|
+
|
|
16
|
+
**Custom Action Deployment:**
|
|
17
|
+
- New custom actions MUST be merged to the main branch before they can be used
|
|
18
|
+
- After the initial merge, they should be tested from feature branches
|
|
19
|
+
|
|
20
|
+
**Debug Steps:**
|
|
21
|
+
Add debug steps that print non-secret parameters when:
|
|
22
|
+
- Creating a new action, OR
|
|
23
|
+
- Troubleshooting a particularly tricky issue
|
|
24
|
+
|
|
25
|
+
(Not required for every workflow - use when needed)
|
|
26
|
+
|
|
27
|
+
## Effectiveness Principles
|
|
28
|
+
|
|
29
|
+
Actions cost CI minutes. Be deliberate, not iterative:
|
|
30
|
+
|
|
31
|
+
1. **Monitor, don't poll** - use `gh run watch` / `gh pr checks --watch` to follow runs live
|
|
32
|
+
2. **Read logs, don't guess** - fetch the failed job's log before changing code
|
|
33
|
+
3. **Print actual values** - debug steps reveal the real `inputs`/`github` context, not your assumptions
|
|
34
|
+
4. **Test locally first** - `act` runs workflows on your machine and avoids burning CI minutes
|
|
35
|
+
5. **Plan the smallest reproduction** - one job, minimal matrix, narrow trigger before scaling up
|
|
36
|
+
|
|
37
|
+
See [README.md](README.md) for the full debugging workflow, `gh` commands, and YAML debug-step examples.
|
|
38
|
+
|
|
39
|
+
## Key Gotchas
|
|
40
|
+
|
|
41
|
+
1. **Secrets unavailable in fork PRs** - `pull_request` has no secrets for forks; `pull_request_target` does but **never check out or execute fork PR code inside it** (RCE with write permissions)
|
|
42
|
+
2. **Pin action versions** - Use `@v4` or SHA, not `@main` (prevents breaking changes)
|
|
43
|
+
3. **Explicit permissions** - Set `permissions:` block for GITHUB_TOKEN operations
|
|
44
|
+
4. **Artifacts for job-to-job data** - Files don't persist between jobs without `upload-artifact`/`download-artifact`
|
|
@@ -41,7 +41,7 @@ event-type selection, cron schedule, and automation creation automatically.
|
|
|
41
41
|
|
|
42
42
|
## Requirements
|
|
43
43
|
|
|
44
|
-
- `
|
|
44
|
+
- `GITHUB_PERSONAL_ACCESS_TOKEN` secret set in OpenHands Settings → Secrets
|
|
45
45
|
- Classic PAT: `repo` (private repos) or `public_repo` (public repos)
|
|
46
46
|
- Fine-grained PAT: Issues — Read and Write
|
|
47
47
|
- The monitored repository must be accessible with that token
|
|
@@ -45,13 +45,13 @@ before proceeding:
|
|
|
45
45
|
|
|
46
46
|
| Secret name | Token type | Minimum permissions |
|
|
47
47
|
|---|---|---|
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
48
|
+
| `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` (private repos) or `public_repo` (public repos) |
|
|
49
|
+
| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Issues: Read and Write |
|
|
50
50
|
|
|
51
51
|
Check with:
|
|
52
52
|
```bash
|
|
53
53
|
curl -s https://api.github.com/user \
|
|
54
|
-
-H "Authorization: Bearer $
|
|
54
|
+
-H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
|
|
55
55
|
-H "Accept: application/vnd.github+json" \
|
|
56
56
|
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))"
|
|
57
57
|
```
|
|
@@ -71,12 +71,12 @@ function without GitHub credentials.
|
|
|
71
71
|
|
|
72
72
|
Follow these steps in order.
|
|
73
73
|
|
|
74
|
-
### Step 1 - Verify
|
|
74
|
+
### Step 1 - Verify GITHUB_PERSONAL_ACCESS_TOKEN
|
|
75
75
|
|
|
76
76
|
Fetch the secret and run the `curl` check above.
|
|
77
77
|
|
|
78
78
|
- If the secret is absent: tell the user
|
|
79
|
-
*"
|
|
79
|
+
*"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in OpenHands Settings → Secrets
|
|
80
80
|
(classic PAT with `repo` or `public_repo` scope, or a fine-grained PAT
|
|
81
81
|
with Issues: Read and Write)."* Then stop.
|
|
82
82
|
|
|
@@ -92,7 +92,7 @@ Validate access and write permissions:
|
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
94
|
curl -s "https://api.github.com/repos/{owner}/{repo}" \
|
|
95
|
-
-H "Authorization: Bearer $
|
|
95
|
+
-H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
|
|
96
96
|
-H "Accept: application/vnd.github+json" \
|
|
97
97
|
| python3 -c "
|
|
98
98
|
import json, sys
|
|
@@ -123,7 +123,7 @@ Record as `TRIGGER_PHRASE`. Default: `"@openhands"`.
|
|
|
123
123
|
### Step 4 - Collect allowed GitHub logins
|
|
124
124
|
|
|
125
125
|
Ask the user: *"Which GitHub users may trigger this automation?
|
|
126
|
-
Press Enter to allow only the authenticated `
|
|
126
|
+
Press Enter to allow only the authenticated `GITHUB_PERSONAL_ACCESS_TOKEN` owner.
|
|
127
127
|
You may also provide comma-separated GitHub logins, or `*` to allow any
|
|
128
128
|
non-bot commenter on the monitored repository."*
|
|
129
129
|
|
|
@@ -261,7 +261,7 @@ Tell the user:
|
|
|
261
261
|
Each cron run executes `main.py`, which:
|
|
262
262
|
|
|
263
263
|
1. **Loads state** from the JSON file (see `references/state-schema.md`).
|
|
264
|
-
2. **Resolves and validates
|
|
264
|
+
2. **Resolves and validates GITHUB_PERSONAL_ACCESS_TOKEN** — aborts immediately if absent or invalid.
|
|
265
265
|
3. **Polls for new events** since the previous `last_poll` timestamp:
|
|
266
266
|
- `GET /repos/{owner}/{repo}/issues/comments?since=…` for `issue_comment`
|
|
267
267
|
- `GET /repos/{owner}/{repo}/pulls/comments?since=…` for `pr_review_comment`
|
|
@@ -307,7 +307,7 @@ Each cron run executes `main.py`, which:
|
|
|
307
307
|
|
|
308
308
|
| Symptom | Likely cause | Fix |
|
|
309
309
|
|---|---|---|
|
|
310
|
-
| Bot doesn't respond to comments | `
|
|
310
|
+
| Bot doesn't respond to comments | `GITHUB_PERSONAL_ACCESS_TOKEN` missing or wrong scopes | Verify token with `curl /user`; check scopes in Step 1 |
|
|
311
311
|
| "Bad credentials" in run logs | Token expired | Rotate token and update the secret in Settings |
|
|
312
312
|
| 404 on repo access | Repo name wrong or token has no access | Re-check `owner/repo` spelling; add token as collaborator |
|
|
313
313
|
| Comments posted but no conversation created | Agent server URL wrong | Check `OPENHANDS_URL` secret and `AGENT_SERVER_URL` env var |
|
|
@@ -10,7 +10,7 @@ the GitHub Repository Monitor automation.
|
|
|
10
10
|
All requests use Bearer authentication:
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
Authorization: Bearer {
|
|
13
|
+
Authorization: Bearer {GITHUB_PERSONAL_ACCESS_TOKEN}
|
|
14
14
|
Accept: application/vnd.github+json
|
|
15
15
|
X-GitHub-Api-Version: 2022-11-28
|
|
16
16
|
```
|
|
@@ -37,7 +37,7 @@ X-GitHub-Api-Version: 2022-11-28
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
curl -I https://api.github.com/user \
|
|
40
|
-
-H "Authorization: Bearer $
|
|
40
|
+
-H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
|
|
41
41
|
| grep -i x-oauth-scopes
|
|
42
42
|
# X-OAuth-Scopes: repo, public_repo
|
|
43
43
|
```
|
|
@@ -209,7 +209,7 @@ Typical usage: **< 20 requests/hour** for a quiet repo,
|
|
|
209
209
|
Check remaining quota with:
|
|
210
210
|
```bash
|
|
211
211
|
curl -s https://api.github.com/rate_limit \
|
|
212
|
-
-H "Authorization: Bearer $
|
|
212
|
+
-H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
|
|
213
213
|
| python3 -c "
|
|
214
214
|
import json, sys
|
|
215
215
|
d = json.load(sys.stdin)
|
|
@@ -18,7 +18,7 @@ Configuration constants are embedded at automation-creation time by the skill.
|
|
|
18
18
|
See SKILL.md for the full setup workflow.
|
|
19
19
|
|
|
20
20
|
Required secrets (set in OpenHands Settings → Secrets):
|
|
21
|
-
|
|
21
|
+
GITHUB_PERSONAL_ACCESS_TOKEN - Personal Access Token
|
|
22
22
|
Classic PAT: 'repo' scope (private) or 'public_repo' (public)
|
|
23
23
|
Fine-grained PAT: Issues: Read and Write
|
|
24
24
|
|
|
@@ -40,7 +40,7 @@ from urllib.parse import urlencode
|
|
|
40
40
|
REPO = "owner/repo" # e.g. "microsoft/vscode"
|
|
41
41
|
TRIGGER_PHRASE = "@openhands" # case-insensitive
|
|
42
42
|
EVENT_TYPES = ["issue_comment"] # e.g. ["issue_comment", "pr_review_comment"]
|
|
43
|
-
# Who may trigger conversations. Default is the authenticated
|
|
43
|
+
# Who may trigger conversations. Default is the authenticated GITHUB_PERSONAL_ACCESS_TOKEN owner.
|
|
44
44
|
# Use ["*"] to allow any non-bot commenter, or explicit logins like ["octocat"].
|
|
45
45
|
ALLOWED_GITHUB_LOGINS = ["<TOKEN_OWNER>"]
|
|
46
46
|
DEFAULT_OPENHANDS_URL = "http://localhost:8000"
|
|
@@ -207,15 +207,15 @@ def _github_paginate(token: str, path: str, params: dict | None = None) -> list:
|
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
def _resolve_github_token() -> str:
|
|
210
|
-
"""Fetch
|
|
210
|
+
"""Fetch GITHUB_PERSONAL_ACCESS_TOKEN from secrets. Raises RuntimeError if absent."""
|
|
211
211
|
try:
|
|
212
|
-
token = get_secret("
|
|
212
|
+
token = get_secret("GITHUB_PERSONAL_ACCESS_TOKEN")
|
|
213
213
|
if token:
|
|
214
214
|
return token
|
|
215
215
|
except Exception:
|
|
216
216
|
pass
|
|
217
217
|
raise RuntimeError(
|
|
218
|
-
"
|
|
218
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN secret is not set. "
|
|
219
219
|
"Go to OpenHands Settings → Secrets and add your GitHub Personal Access Token."
|
|
220
220
|
)
|
|
221
221
|
|
|
@@ -231,7 +231,7 @@ def _verify_token_and_repo(token: str, repo: str) -> str:
|
|
|
231
231
|
except urllib.error.HTTPError as exc:
|
|
232
232
|
if exc.code == 401:
|
|
233
233
|
raise RuntimeError(
|
|
234
|
-
"
|
|
234
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN is invalid or expired. "
|
|
235
235
|
"Update it in OpenHands Settings → Secrets."
|
|
236
236
|
)
|
|
237
237
|
raise RuntimeError(f"GitHub /user check failed: {exc.code}")
|
|
@@ -247,13 +247,13 @@ def _verify_token_and_repo(token: str, repo: str) -> str:
|
|
|
247
247
|
except urllib.error.HTTPError as exc:
|
|
248
248
|
if exc.code == 404:
|
|
249
249
|
raise RuntimeError(
|
|
250
|
-
f"Repository '{repo}' not found or not accessible with the current
|
|
250
|
+
f"Repository '{repo}' not found or not accessible with the current GITHUB_PERSONAL_ACCESS_TOKEN. "
|
|
251
251
|
"Check the repo name (format: owner/repo) and token permissions."
|
|
252
252
|
)
|
|
253
253
|
if exc.code == 403:
|
|
254
254
|
raise RuntimeError(
|
|
255
255
|
f"Access denied to repository '{repo}'. "
|
|
256
|
-
"Ensure
|
|
256
|
+
"Ensure GITHUB_PERSONAL_ACCESS_TOKEN has the required permissions."
|
|
257
257
|
)
|
|
258
258
|
raise RuntimeError(f"GitHub /repos/{repo} check failed: {exc.code}")
|
|
259
259
|
|
|
@@ -268,7 +268,7 @@ def _verify_token_and_repo(token: str, repo: str) -> str:
|
|
|
268
268
|
# Private repo: must have push access or the 'repo' classic-PAT scope.
|
|
269
269
|
if not can_push and not has_repo_scope and scopes:
|
|
270
270
|
raise RuntimeError(
|
|
271
|
-
f"
|
|
271
|
+
f"GITHUB_PERSONAL_ACCESS_TOKEN cannot post comments to private repository '{repo}'. "
|
|
272
272
|
"A classic PAT needs the 'repo' scope; "
|
|
273
273
|
"a fine-grained PAT needs 'Issues: Read and Write' permission."
|
|
274
274
|
)
|
|
@@ -276,7 +276,7 @@ def _verify_token_and_repo(token: str, repo: str) -> str:
|
|
|
276
276
|
# Public repo: need at minimum 'public_repo' scope or push access.
|
|
277
277
|
if scopes and not (can_push or has_public_repo_scope or has_repo_scope):
|
|
278
278
|
raise RuntimeError(
|
|
279
|
-
f"
|
|
279
|
+
f"GITHUB_PERSONAL_ACCESS_TOKEN cannot post comments to public repository '{repo}'. "
|
|
280
280
|
"A classic PAT needs the 'public_repo' scope; "
|
|
281
281
|
"a fine-grained PAT needs 'Issues: Read and Write' permission."
|
|
282
282
|
)
|
|
@@ -591,7 +591,7 @@ def _build_initial_prompt(ctx: dict, trigger_comment: dict, event_type: str) ->
|
|
|
591
591
|
f"\nTriggering comment by @{trigger_author}:{path_info}\n"
|
|
592
592
|
f"---\n{trigger_body}\n---\n"
|
|
593
593
|
f"\nPlease analyse the request and take the appropriate action.\n"
|
|
594
|
-
f"The
|
|
594
|
+
f"The GITHUB_PERSONAL_ACCESS_TOKEN secret is available if you need to interact with the "
|
|
595
595
|
f"GitHub API (fetch the PR diff, create commits, update labels, etc.).\n"
|
|
596
596
|
f"When you are finished, summarise what you did clearly — that summary "
|
|
597
597
|
f"will be posted back to the GitHub {item_type} as a comment."
|
package/skills/index.js
CHANGED
|
@@ -159,6 +159,18 @@ export const SKILLS_CATALOG = [
|
|
|
159
159
|
],
|
|
160
160
|
"content": "You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with\nthe GitHub API.\n\n<IMPORTANT>\nYou can use `curl` with the `GITHUB_TOKEN` to interact with GitHub's API.\nALWAYS use the GitHub API for operations instead of a web browser.\nALWAYS use the `create_pr` tool to open a pull request\nIf the user asks you to check GitHub Actions status, first try to use `gh` to work with workflows, and only fallback to basic API calls if that fails.\nExamples:\n- `gh run watch` (https://cli.github.com/manual/gh_run_watch) to monitor workflow runs\n- `gh pr checks 200 --watch --interval 10` to check until completed.\n</IMPORTANT>\n\nIf you encounter authentication issues when pushing to GitHub (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git`\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Use the `create_pr` tool to create a pull request, if you haven't already\n* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a pull request, send the user a short message with a link to the pull request.\n* Do NOT mark a pull request as ready to review unless the user explicitly says so\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```\n\n## Handling Review Comments\n\n- Critically evaluate each review comment before acting on it. Not all feedback is worth implementing:\n - Does it fix a real bug or improve clarity significantly?\n - Does it align with the project's engineering principles (simplicity, maintainability)?\n - Is the suggested change proportional to the benefit, or does it add unnecessary complexity?\n- It's acceptable to respectfully decline suggestions that add verbosity without clear benefit, over-engineer for hypothetical edge cases, or contradict the project's pragmatic approach.\n- After addressing (or deciding not to address) inline review comments, mark the corresponding review threads as resolved.\n- Before resolving a thread, leave a reply comment that either explains the reason for dismissing the feedback or references the specific commit (e.g., commit SHA) that addressed the issue.\n- Prefer resolving threads only once fixes are pushed or a clear decision is documented.\n- Use the GitHub GraphQL API to reply to and resolve review threads (see below).\n- After making changes to a PR, verify the title and description still match the content. Update them if the scope, features, or intent changed.\n\n## Resolving Review Threads via GraphQL\n\nTo resolve existing review threads programmatically:\n\n1. Get the thread IDs (replace `<OWNER>`, `<REPO>`, `<PR_NUMBER>`):\n```bash\ngh api graphql -f query='\n{\n repository(owner: \"<OWNER>\", name: \"<REPO>\") {\n pullRequest(number: <PR_NUMBER>) {\n reviewThreads(first: 20) {\n nodes {\n id\n isResolved\n comments(first: 1) {\n nodes { body }\n }\n }\n }\n }\n }\n}'\n```\n\n2. Reply to the thread explaining how the feedback was addressed:\n```bash\ngh api graphql -f query='\nmutation {\n addPullRequestReviewThreadReply(input: {\n pullRequestReviewThreadId: \"<THREAD_ID>\"\n body: \"Fixed in <COMMIT_SHA>\"\n }) {\n comment { id }\n }\n}'\n```\n\n3. Resolve the thread:\n```bash\ngh api graphql -f query='\nmutation {\n resolveReviewThread(input: {threadId: \"<THREAD_ID>\"}) {\n thread { isResolved }\n }\n}'\n```\n\n4. Get the failed workflow run ID and rerun it:\n```bash\n# Find the run ID from the failed check URL, or use:\ngh run list --repo <OWNER>/<REPO> --branch <BRANCH> --limit 5\n\n# Rerun failed jobs\ngh run rerun <RUN_ID> --repo <OWNER>/<REPO> --failed\n```"
|
|
161
161
|
},
|
|
162
|
+
{
|
|
163
|
+
"name": "github-actions",
|
|
164
|
+
"description": "Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating workflows, or troubleshooting GitHub Actions.",
|
|
165
|
+
"triggers": [
|
|
166
|
+
"github actions",
|
|
167
|
+
"github workflow",
|
|
168
|
+
"actions workflow",
|
|
169
|
+
"gh actions",
|
|
170
|
+
".github/workflows"
|
|
171
|
+
],
|
|
172
|
+
"content": "# GitHub Actions Guide\n\n## Critical Rules\n\n**Custom Action Deployment:**\n- New custom actions MUST be merged to the main branch before they can be used\n- After the initial merge, they should be tested from feature branches\n\n**Debug Steps:**\nAdd debug steps that print non-secret parameters when:\n- Creating a new action, OR\n- Troubleshooting a particularly tricky issue\n\n(Not required for every workflow - use when needed)\n\n## Effectiveness Principles\n\nActions cost CI minutes. Be deliberate, not iterative:\n\n1. **Monitor, don't poll** - use `gh run watch` / `gh pr checks --watch` to follow runs live\n2. **Read logs, don't guess** - fetch the failed job's log before changing code\n3. **Print actual values** - debug steps reveal the real `inputs`/`github` context, not your assumptions\n4. **Test locally first** - `act` runs workflows on your machine and avoids burning CI minutes\n5. **Plan the smallest reproduction** - one job, minimal matrix, narrow trigger before scaling up\n\nSee [README.md](README.md) for the full debugging workflow, `gh` commands, and YAML debug-step examples.\n\n## Key Gotchas\n\n1. **Secrets unavailable in fork PRs** - `pull_request` has no secrets for forks; `pull_request_target` does but **never check out or execute fork PR code inside it** (RCE with write permissions)\n2. **Pin action versions** - Use `@v4` or SHA, not `@main` (prevents breaking changes)\n3. **Explicit permissions** - Set `permissions:` block for GITHUB_TOKEN operations\n4. **Artifacts for job-to-job data** - Files don't persist between jobs without `upload-artifact`/`download-artifact`"
|
|
173
|
+
},
|
|
162
174
|
{
|
|
163
175
|
"name": "github-pr-review",
|
|
164
176
|
"description": "Post PR review comments using the GitHub API with inline comments, suggestions, and priority labels.",
|
|
@@ -181,7 +193,7 @@ export const SKILLS_CATALOG = [
|
|
|
181
193
|
"triggers": [
|
|
182
194
|
"/github-monitor:poll"
|
|
183
195
|
],
|
|
184
|
-
"content": "# GitHub Repository Monitor\n\nCreate a cron automation that polls a single GitHub repository on a\nconfigurable schedule (default: every minute).\n\nWhen a comment on an issue or PR contains the **trigger phrase**\n(default: `@OpenHands`) it:\n\n1. Posts a GitHub comment acknowledging the request with a conversation link.\n2. Creates an OpenHands conversation pre-loaded with the issue/PR title, body,\n labels, and recent comment history for full context.\n3. Posts a summary GitHub comment when the conversation finishes.\n\nOn every subsequent run:\n- New trigger comments on an already-tracked issue/PR are forwarded to the\n running conversation (or re-open a previously closed one).\n- When a conversation goes idle/finished/error the agent's final response\n is posted back as a GitHub comment.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secret\n\nVerify that the following secret is set in **OpenHands Settings → Secrets**\nbefore proceeding:\n\n| Secret name | Token type | Minimum permissions |\n|---|---|---|\n| `GITHUB_TOKEN` | Classic PAT | `repo` (private repos) or `public_repo` (public repos) |\n| `GITHUB_TOKEN` | Fine-grained PAT | Issues: Read and Write |\n\nCheck with:\n```bash\ncurl -s https://api.github.com/user \\\n -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))\"\n```\n\nIf the token is missing, inform the user and stop — the automation cannot\nfunction without GitHub credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links in GitHub comments |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Verify GITHUB_TOKEN\n\nFetch the secret and run the `curl` check above.\n\n- If the secret is absent: tell the user\n *\"GITHUB_TOKEN is not set. Please add it in OpenHands Settings → Secrets\n (classic PAT with `repo` or `public_repo` scope, or a fine-grained PAT\n with Issues: Read and Write).\"* Then stop.\n\n- If the API returns a non-200 or `{\"message\": \"Bad credentials\"}`:\n tell the user the token is invalid and ask them to update it.\n\n### Step 2 - Collect repository\n\nAsk the user: *\"Which GitHub repository should be monitored?\n(Format: `owner/repo`, e.g. `microsoft/vscode`)\"*\n\nValidate access and write permissions:\n\n```bash\ncurl -s \"https://api.github.com/repos/{owner}/{repo}\" \\\n -H \"Authorization: Bearer $GITHUB_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\n | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\nif 'message' in d:\n print('ERROR:', d['message'])\nelse:\n perms = d.get('permissions', {})\n print(f\\\"Accessible. Private: {d.get('private')}. Permissions: {perms}\\\")\n\"\n```\n\n- If `message: Not Found` or `message: Bad credentials` →\n inform the user and ask them to check the repo name and token.\n- If the repo is private and `permissions.push` is `false` →\n inform the user the token does not have write access and comments will fail.\n- If the check passes, record `REPO = \"{owner}/{repo}\"`.\n\n### Step 3 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@OpenHands`)\"*\n\nAccepted values: any non-empty string unlikely to appear by accident.\n\nRecord as `TRIGGER_PHRASE`. Default: `\"@openhands\"`.\n\n### Step 4 - Collect allowed GitHub logins\n\nAsk the user: *\"Which GitHub users may trigger this automation?\nPress Enter to allow only the authenticated `GITHUB_TOKEN` owner.\nYou may also provide comma-separated GitHub logins, or `*` to allow any\nnon-bot commenter on the monitored repository.\"*\n\nMap the answer to `ALLOWED_GITHUB_LOGINS`:\n\n| User answer | `ALLOWED_GITHUB_LOGINS` value |\n|---|---|\n| Empty/default | `[\"<TOKEN_OWNER>\"]` |\n| `enyst,tofarr` | `[\"enyst\", \"tofarr\"]` |\n| `*` | `[\"*\"]` |\n\nDefault to token-owner-only unless the user explicitly chooses a broader\nallowlist. Record as `ALLOWED_GITHUB_LOGINS`.\n\n### Step 5 - Collect event types\n\nAsk the user: *\"Which event types should be monitored?\nChoose one or more:*\n *1. Issue and PR comments (default)*\n *2. PR inline review comments*\n *3. Both*\n*(Press Enter to accept the default: issue and PR comments.)\"*\n\nMap the choice to the `EVENT_TYPES` list:\n\n| Choice | `EVENT_TYPES` value |\n|---|---|\n| 1 (default) | `[\"issue_comment\"]` |\n| 2 | `[\"pr_review_comment\"]` |\n| 3 | `[\"issue_comment\", \"pr_review_comment\"]` |\n\n### Step 6 - Collect cron schedule\n\nAsk the user: *\"How often should the automation poll GitHub?\n(Press Enter for the default: every minute.\nUse a cron expression for a different interval, e.g.:\n`*/5 * * * *` = every 5 minutes,\n`0 * * * *` = every hour)\"*\n\nDefault: `* * * * *` (every minute).\n\nRecord as `CRON_SCHEDULE`.\n\n### Step 7 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly five\nconstant substitutions near the top of the file:\n\n| Placeholder | Replace with |\n|---|---|\n| `REPO = \"owner/repo\"` | `REPO = \"{owner_repo}\"` |\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{trigger_phrase_lower}\"` |\n| `EVENT_TYPES = [\"issue_comment\"]` | `EVENT_TYPES = {event_types_list}` |\n| `ALLOWED_GITHUB_LOGINS = [\"<TOKEN_OWNER>\"]` | `ALLOWED_GITHUB_LOGINS = {allowed_logins_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if the user has no preference) |\n\nWrite the customised script to a temporary build directory:\n```bash\nmkdir -p /tmp/github-monitor-build\n# (write the customised main.py to /tmp/github-monitor-build/main.py)\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/github-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\n\n### Step 8 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/github-monitor.tar.gz -C /tmp/github-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-repo-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/github-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\n### Step 9 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"GitHub Monitor: {owner}/{repo}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 10 - Confirm\n\nTell the user:\n\n> ✅ **GitHub Repository Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Repository: `{owner}/{repo}`\n> - Trigger phrase: `{phrase}`\n> - Event types: `{event_types}`\n> - Allowed GitHub logins: `{allowed_logins}`\n> - Polling schedule: `{cron_schedule}`\n> - State file: `~/.openhands/workspaces/automation-state/github_poller_{id}.json`\n>\n> From an allowed GitHub login, post a comment containing `{phrase}` on any\n> issue or PR in `{owner}/{repo}` to test it. OpenHands will acknowledge with\n> a comment and a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves and validates GITHUB_TOKEN** — aborts immediately if absent or invalid.\n3. **Polls for new events** since the previous `last_poll` timestamp:\n - `GET /repos/{owner}/{repo}/issues/comments?since=…` for `issue_comment`\n - `GET /repos/{owner}/{repo}/pulls/comments?since=…` for `pr_review_comment`\n4. **Processes matching comments** in chronological order:\n - Skips bot accounts (login ending in `[bot]`) to avoid feedback loops.\n - Skips already-processed comment IDs.\n - Skips comments from logins outside `ALLOWED_GITHUB_LOGINS`.\n - Checks body for the trigger phrase (case-insensitive).\n - Extracts the issue/PR number from the comment URL.\n5. **For each trigger comment**, per issue/PR:\n - **Active conversation** → forwards the new comment directly.\n - **Closed conversation** → tries to re-open it; falls back to creating\n a new conversation if the old one is unreachable.\n - **No conversation** → fetches full context (title, body, labels, last\n 10 comments) and creates a new conversation with a detailed prompt.\n - Posts a GitHub comment: *\"🤖 OpenHands is on it! View progress: {url}\"*\n6. **Checks active conversations** for completion:\n - If `status ∈ {idle, finished, error, stuck}` and enough time has passed\n since creation (debounce), fetches the agent's final response and posts\n it as a GitHub comment. Marks the conversation `closed`.\n7. **Saves state** and fires the completion callback.\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n and conversation lifecycle diagram.\n- **`references/github-api.md`** - GitHub API endpoint reference, token\n scopes, rate limits, and common error codes.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the four\n constants at the top (`REPO`, `TRIGGER_PHRASE`, `EVENT_TYPES`,\n `DEFAULT_OPENHANDS_URL`) before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't respond to comments | `GITHUB_TOKEN` missing or wrong scopes | Verify token with `curl /user`; check scopes in Step 1 |\n| \"Bad credentials\" in run logs | Token expired | Rotate token and update the secret in Settings |\n| 404 on repo access | Repo name wrong or token has no access | Re-check `owner/repo` spelling; add token as collaborator |\n| Comments posted but no conversation created | Agent server URL wrong | Check `OPENHANDS_URL` secret and `AGENT_SERVER_URL` env var |\n| Same comment processed twice | `processed_comment_ids` cleared | State file was deleted; harmless but duplicate comment may appear |\n| Summary never posted | Conversation stuck in `running` | Open the conversation in the OpenHands UI; agent may need input |\n| No events detected after first run | `last_poll` in the future | Delete the state file to reset; it will be recreated on next run |"
|
|
196
|
+
"content": "# GitHub Repository Monitor\n\nCreate a cron automation that polls a single GitHub repository on a\nconfigurable schedule (default: every minute).\n\nWhen a comment on an issue or PR contains the **trigger phrase**\n(default: `@OpenHands`) it:\n\n1. Posts a GitHub comment acknowledging the request with a conversation link.\n2. Creates an OpenHands conversation pre-loaded with the issue/PR title, body,\n labels, and recent comment history for full context.\n3. Posts a summary GitHub comment when the conversation finishes.\n\nOn every subsequent run:\n- New trigger comments on an already-tracked issue/PR are forwarded to the\n running conversation (or re-open a previously closed one).\n- When a conversation goes idle/finished/error the agent's final response\n is posted back as a GitHub comment.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secret\n\nVerify that the following secret is set in **OpenHands Settings → Secrets**\nbefore proceeding:\n\n| Secret name | Token type | Minimum permissions |\n|---|---|---|\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` (private repos) or `public_repo` (public repos) |\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Issues: Read and Write |\n\nCheck with:\n```bash\ncurl -s https://api.github.com/user \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))\"\n```\n\nIf the token is missing, inform the user and stop — the automation cannot\nfunction without GitHub credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links in GitHub comments |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Verify GITHUB_PERSONAL_ACCESS_TOKEN\n\nFetch the secret and run the `curl` check above.\n\n- If the secret is absent: tell the user\n *\"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in OpenHands Settings → Secrets\n (classic PAT with `repo` or `public_repo` scope, or a fine-grained PAT\n with Issues: Read and Write).\"* Then stop.\n\n- If the API returns a non-200 or `{\"message\": \"Bad credentials\"}`:\n tell the user the token is invalid and ask them to update it.\n\n### Step 2 - Collect repository\n\nAsk the user: *\"Which GitHub repository should be monitored?\n(Format: `owner/repo`, e.g. `microsoft/vscode`)\"*\n\nValidate access and write permissions:\n\n```bash\ncurl -s \"https://api.github.com/repos/{owner}/{repo}\" \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n -H \"Accept: application/vnd.github+json\" \\\n | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\nif 'message' in d:\n print('ERROR:', d['message'])\nelse:\n perms = d.get('permissions', {})\n print(f\\\"Accessible. Private: {d.get('private')}. Permissions: {perms}\\\")\n\"\n```\n\n- If `message: Not Found` or `message: Bad credentials` →\n inform the user and ask them to check the repo name and token.\n- If the repo is private and `permissions.push` is `false` →\n inform the user the token does not have write access and comments will fail.\n- If the check passes, record `REPO = \"{owner}/{repo}\"`.\n\n### Step 3 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@OpenHands`)\"*\n\nAccepted values: any non-empty string unlikely to appear by accident.\n\nRecord as `TRIGGER_PHRASE`. Default: `\"@openhands\"`.\n\n### Step 4 - Collect allowed GitHub logins\n\nAsk the user: *\"Which GitHub users may trigger this automation?\nPress Enter to allow only the authenticated `GITHUB_PERSONAL_ACCESS_TOKEN` owner.\nYou may also provide comma-separated GitHub logins, or `*` to allow any\nnon-bot commenter on the monitored repository.\"*\n\nMap the answer to `ALLOWED_GITHUB_LOGINS`:\n\n| User answer | `ALLOWED_GITHUB_LOGINS` value |\n|---|---|\n| Empty/default | `[\"<TOKEN_OWNER>\"]` |\n| `enyst,tofarr` | `[\"enyst\", \"tofarr\"]` |\n| `*` | `[\"*\"]` |\n\nDefault to token-owner-only unless the user explicitly chooses a broader\nallowlist. Record as `ALLOWED_GITHUB_LOGINS`.\n\n### Step 5 - Collect event types\n\nAsk the user: *\"Which event types should be monitored?\nChoose one or more:*\n *1. Issue and PR comments (default)*\n *2. PR inline review comments*\n *3. Both*\n*(Press Enter to accept the default: issue and PR comments.)\"*\n\nMap the choice to the `EVENT_TYPES` list:\n\n| Choice | `EVENT_TYPES` value |\n|---|---|\n| 1 (default) | `[\"issue_comment\"]` |\n| 2 | `[\"pr_review_comment\"]` |\n| 3 | `[\"issue_comment\", \"pr_review_comment\"]` |\n\n### Step 6 - Collect cron schedule\n\nAsk the user: *\"How often should the automation poll GitHub?\n(Press Enter for the default: every minute.\nUse a cron expression for a different interval, e.g.:\n`*/5 * * * *` = every 5 minutes,\n`0 * * * *` = every hour)\"*\n\nDefault: `* * * * *` (every minute).\n\nRecord as `CRON_SCHEDULE`.\n\n### Step 7 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly five\nconstant substitutions near the top of the file:\n\n| Placeholder | Replace with |\n|---|---|\n| `REPO = \"owner/repo\"` | `REPO = \"{owner_repo}\"` |\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{trigger_phrase_lower}\"` |\n| `EVENT_TYPES = [\"issue_comment\"]` | `EVENT_TYPES = {event_types_list}` |\n| `ALLOWED_GITHUB_LOGINS = [\"<TOKEN_OWNER>\"]` | `ALLOWED_GITHUB_LOGINS = {allowed_logins_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if the user has no preference) |\n\nWrite the customised script to a temporary build directory:\n```bash\nmkdir -p /tmp/github-monitor-build\n# (write the customised main.py to /tmp/github-monitor-build/main.py)\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/github-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\n\n### Step 8 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/github-monitor.tar.gz -C /tmp/github-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-repo-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/github-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\n### Step 9 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"GitHub Monitor: {owner}/{repo}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 10 - Confirm\n\nTell the user:\n\n> ✅ **GitHub Repository Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Repository: `{owner}/{repo}`\n> - Trigger phrase: `{phrase}`\n> - Event types: `{event_types}`\n> - Allowed GitHub logins: `{allowed_logins}`\n> - Polling schedule: `{cron_schedule}`\n> - State file: `~/.openhands/workspaces/automation-state/github_poller_{id}.json`\n>\n> From an allowed GitHub login, post a comment containing `{phrase}` on any\n> issue or PR in `{owner}/{repo}` to test it. OpenHands will acknowledge with\n> a comment and a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves and validates GITHUB_PERSONAL_ACCESS_TOKEN** — aborts immediately if absent or invalid.\n3. **Polls for new events** since the previous `last_poll` timestamp:\n - `GET /repos/{owner}/{repo}/issues/comments?since=…` for `issue_comment`\n - `GET /repos/{owner}/{repo}/pulls/comments?since=…` for `pr_review_comment`\n4. **Processes matching comments** in chronological order:\n - Skips bot accounts (login ending in `[bot]`) to avoid feedback loops.\n - Skips already-processed comment IDs.\n - Skips comments from logins outside `ALLOWED_GITHUB_LOGINS`.\n - Checks body for the trigger phrase (case-insensitive).\n - Extracts the issue/PR number from the comment URL.\n5. **For each trigger comment**, per issue/PR:\n - **Active conversation** → forwards the new comment directly.\n - **Closed conversation** → tries to re-open it; falls back to creating\n a new conversation if the old one is unreachable.\n - **No conversation** → fetches full context (title, body, labels, last\n 10 comments) and creates a new conversation with a detailed prompt.\n - Posts a GitHub comment: *\"🤖 OpenHands is on it! View progress: {url}\"*\n6. **Checks active conversations** for completion:\n - If `status ∈ {idle, finished, error, stuck}` and enough time has passed\n since creation (debounce), fetches the agent's final response and posts\n it as a GitHub comment. Marks the conversation `closed`.\n7. **Saves state** and fires the completion callback.\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n and conversation lifecycle diagram.\n- **`references/github-api.md`** - GitHub API endpoint reference, token\n scopes, rate limits, and common error codes.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the four\n constants at the top (`REPO`, `TRIGGER_PHRASE`, `EVENT_TYPES`,\n `DEFAULT_OPENHANDS_URL`) before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't respond to comments | `GITHUB_PERSONAL_ACCESS_TOKEN` missing or wrong scopes | Verify token with `curl /user`; check scopes in Step 1 |\n| \"Bad credentials\" in run logs | Token expired | Rotate token and update the secret in Settings |\n| 404 on repo access | Repo name wrong or token has no access | Re-check `owner/repo` spelling; add token as collaborator |\n| Comments posted but no conversation created | Agent server URL wrong | Check `OPENHANDS_URL` secret and `AGENT_SERVER_URL` env var |\n| Same comment processed twice | `processed_comment_ids` cleared | State file was deleted; harmless but duplicate comment may appear |\n| Summary never posted | Conversation stuck in `running` | Open the conversation in the OpenHands UI; agent may need input |\n| No events detected after first run | `last_poll` in the future | Delete the state file to reset; it will be recreated on next run |"
|
|
185
197
|
},
|
|
186
198
|
{
|
|
187
199
|
"name": "gitlab",
|
|
@@ -386,7 +398,7 @@ export const SKILLS_CATALOG = [
|
|
|
386
398
|
"triggers": [
|
|
387
399
|
"/slack-monitor:poll"
|
|
388
400
|
],
|
|
389
|
-
"content": "# Slack Channel Monitor\n\nCreate a cron automation that polls up to 10 Slack channels every minute.\nWhen a message containing the **trigger phrase** (default: `@openhands`) is\ndetected it:\n\n1. Adds a 👀 reaction to the triggering message.\n2. Opens an OpenHands conversation with the message and recent channel context.\n3. Posts a reply in the Slack thread with a link to the conversation.\n\nOn every subsequent run:\n- Replies in the thread are forwarded to the running conversation.\n- When the conversation finishes (or errors), the agent's final response is\n posted back to the Slack thread.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook-based variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secrets\n\nVerify that at least one of the following secrets is set in\n**OpenHands Settings → Secrets** before proceeding:\n\n| Secret name | Token type | Minimum scopes |\n|---|---|---|\n| `SLACK_BOT_TOKEN` | Bot (`xoxb-…`) | `channels:history`, `channels:read`, `reactions:write`, `chat:write` |\n| `SLACK_USER_TOKEN` | User (`xoxp-…`) | Same as bot, plus `search:read` for multi-channel efficiency |\n\nCheck with:\n```bash\n# For bot token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_BOT_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n\n# For user token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_USER_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n```\n\nIf neither token is present, inform the user and stop - the automation cannot\nfunction without Slack credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Collect channels\n\nAsk the user: *\"Which Slack channels should be monitored? You can provide\nchannel names (e.g. `#general`) or IDs (e.g. `C0123456789`).\"*\n\n**If the user provides channel names**, resolve them to IDs:\n\n```bash\nSLACK_TOKEN=\"${SLACK_BOT_TOKEN:-$SLACK_USER_TOKEN}\"\ncurl -s \"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200&exclude_archived=true\" \\\n -H \"Authorization: Bearer $SLACK_TOKEN\" \\\n | python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\nif not data.get('ok'):\n print('ERROR:', data.get('error'))\n exit(1)\nnames = set(n.lstrip('#') for n in ['CHANNEL_NAMES_HERE'.split(',')])\nfor ch in data.get('channels', []):\n if ch['name'] in names:\n print(f\\\"{ch['name']} → {ch['id']}\\\")\n\"\n```\n\nReplace `CHANNEL_NAMES_HERE` with the comma-separated names the user provided.\n\n**If `conversations.list` returns `missing_scope` or `not_authed`:**\nInform the user: *\"The token doesn't have permission to list channels. Please\nprovide the channel IDs directly (right-click a channel in Slack → Copy link - \nthe last path segment starting with `C` is the ID).\"*\n\n**If the bot token lacks `channels:read`** for private channels, the user can\neither invite the bot first (`/invite @botname`) or switch to a user token.\n\nCollect up to 10 channel IDs. Record them as a Python list literal, e.g.:\n```python\n[\"C0123456789\", \"C9876543210\"]\n```\n\n### Step 2 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@openhands`)\"*\n\nAccepted values: any non-empty string unlikely to appear accidentally, e.g.\n`@openhands`, `jazz hands`, `take-me-to-funky-town`.\n\n### Step 3 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly three\nconstant substitutions near the top of the file:\n\n| Placeholder | Replace with |\n|---|---|\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{user_phrase}\"` |\n| `CHANNEL_IDS: list[str] = []` | `CHANNEL_IDS: list[str] = {channel_id_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if user has no preference) |\n\nWrite the customised script to a temporary directory:\n```bash\nmkdir -p /tmp/slack-monitor-build\n# (write the customised main.py to /tmp/slack-monitor-build/main.py)\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/slack-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\n\n### Step 4 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/slack-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\nIf the upload fails with a size error, the tarball must be under 1 MB.\n`main.py` is under 15 KB so this should never trigger.\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Slack Channel Monitor\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"* * * * *\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nA 55-second timeout keeps runs well within the 60-second cron window.\n\nRecord the returned `id` - share it with the user as confirmation.\n\n### Step 6 - Confirm\n\nTell the user:\n\n> ✅ **Slack Channel Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Channels: `{channel list}`\n> - Trigger phrase: `{phrase}`\n> - Polling every minute via cron `* * * * *`\n> - State file: `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`\n>\n> Send a message containing `{phrase}` in any monitored channel to test it.\n> The bot will react with 👀 and reply with a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which runs **10 polling iterations** (every\n5 seconds) within the 55-second timeout window. Each iteration:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.\n3. **Fetches new messages:**\n - User token + `search:read` + > 1 channel → single `search.messages` call\n (searches for the trigger phrase across all channels).\n - Otherwise → one `conversations.history` call per channel.\n4. **Fetches thread replies** - one `conversations.replies` call per tracked thread.\n5. **Processes messages** in chronological order:\n - Skips messages already in `processed_ts` (dedup across the overlap window).\n - Skips bot messages and any `ts` in `bot_message_ts`.\n - Reply in a tracked thread (active **or** closed) → forwards to the existing\n conversation; re-opens closed conversations automatically.\n - Contains trigger phrase → 👀 reaction, create conversation, post link.\n - Thread replies: agent receives full thread history for context.\n - Root messages: agent receives the trigger text only.\n6. **Checks conversation statuses** - for each active conversation where\n `time.time() - last_activity > 15 s`:\n - If status is `idle`, `finished`, `error`, or `stuck` → fetch the agent's\n final response via `/api/conversations/{id}/agent_final_response` and post\n it to the Slack thread. Mark conversation `closed`.\n7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary\n races). If a conversation creation failed, pins `last_poll` further back to\n retry on the next iteration.\n8. **Saves state** (including `processed_ts`) and continues to the next iteration.\n9. After all iterations, fires the completion callback.\n\nDebug output is written to both stdout and a persistent log at:\n```\n{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log\n```\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/slack-api.md`** - Slack token types, required scopes, API\n endpoint reference, rate limits, and common error codes.\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n example file, and conversation lifecycle diagram.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the three\n constants at the top (`TRIGGER_PHRASE`, `CHANNEL_IDS`, `DEFAULT_OPENHANDS_URL`)\n before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't react to messages | Token missing or bot not in channel | Verify token with `auth.test`; `/invite @botname` |\n| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |\n| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |\n| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |\n| Conversation link 404 | `OPENHANDS_URL` points to wrong host | Set the `OPENHANDS_URL` secret to the correct base URL |\n| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |\n| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |\n| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |\n| Debug info needed | Need detailed per-message trace | Check `{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log` |"
|
|
401
|
+
"content": "# Slack Channel Monitor\n\nCreate a cron automation that polls up to 10 Slack channels every minute.\nWhen a message containing the **trigger phrase** (default: `@openhands`) is\ndetected it:\n\n1. Adds a 👀 reaction to the triggering message.\n2. Opens an OpenHands conversation with the message and recent channel context.\n3. Posts a reply in the Slack thread with a link to the conversation.\n\nOn every subsequent run:\n- Replies in the thread are forwarded to the running conversation.\n- When the conversation finishes (or errors), the agent's final response is\n posted back to the Slack thread.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook-based variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secrets\n\nVerify that at least one of the following secrets is set in\n**OpenHands Settings → Secrets** before proceeding:\n\n| Secret name | Token type | Minimum scopes |\n|---|---|---|\n| `SLACK_BOT_TOKEN` | Bot (`xoxb-…`) | `channels:history`, `channels:read`, `reactions:write`, `chat:write` |\n| `SLACK_USER_TOKEN` | User (`xoxp-…`) | Same as bot, plus `search:read` for multi-channel efficiency |\n\nCheck with:\n```bash\n# For bot token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_BOT_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n\n# For user token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_USER_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n```\n\nIf neither token is present, inform the user and stop - the automation cannot\nfunction without Slack credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Collect channels\n\nAsk the user: *\"Which Slack channels should be monitored? You can provide\nchannel names (e.g. `#general`) or IDs (e.g. `C0123456789`).\"*\n\n**If the user provides channel names**, resolve them to IDs:\n\n```bash\nSLACK_TOKEN=\"${SLACK_BOT_TOKEN:-$SLACK_USER_TOKEN}\"\ncurl -s \"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200&exclude_archived=true\" \\\n -H \"Authorization: Bearer $SLACK_TOKEN\" \\\n | python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\nif not data.get('ok'):\n print('ERROR:', data.get('error'))\n exit(1)\nnames = set(n.lstrip('#') for n in ['CHANNEL_NAMES_HERE'.split(',')])\nfor ch in data.get('channels', []):\n if ch['name'] in names:\n print(f\\\"{ch['name']} → {ch['id']}\\\")\n\"\n```\n\nReplace `CHANNEL_NAMES_HERE` with the comma-separated names the user provided.\n\n**If `conversations.list` returns `missing_scope` or `not_authed`:**\nInform the user: *\"The token doesn't have permission to list channels. Please\nprovide the channel IDs directly (right-click a channel in Slack → Copy link - \nthe last path segment starting with `C` is the ID).\"*\n\n**If the bot token lacks `channels:read`** for private channels, the user can\neither invite the bot first (`/invite @botname`) or switch to a user token.\n\nCollect up to 10 channel IDs. Record them as a Python list literal, e.g.:\n```python\n[\"C0123456789\", \"C9876543210\"]\n```\n\n### Step 2 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@openhands`)\"*\n\nAccepted values: any non-empty string unlikely to appear accidentally, e.g.\n`@openhands`, `jazz hands`, `take-me-to-funky-town`.\n\n### Step 3 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory and **copy it verbatim**.\nApply exactly three constant substitutions near the top of the file:\n\n> **Do not reimplement, simplify, or hand-write a replacement script.**\n> The template already contains the correct secret-loading, state-path,\n> conversation-creation, and context-forwarding logic. Only the three\n> configuration constants below should change unless syntax validation fails.\n\n| Placeholder | Replace with |\n|---|---|\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{user_phrase}\"` |\n| `CHANNEL_IDS: list[str] = []` | `CHANNEL_IDS: list[str] = {channel_id_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if user has no preference) |\n\nWrite the customised script to a temporary directory:\n```bash\nmkdir -p /tmp/slack-monitor-build\n# copy scripts/main.py to /tmp/slack-monitor-build/main.py\n# then replace only the three constants above\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/slack-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nThen run a quick integrity check to confirm the template structure is still\npresent and only the configuration block was customised:\n```bash\ngrep -n 'TRIGGER_PHRASE = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'CHANNEL_IDS: list\\[str\\] =' /tmp/slack-monitor-build/main.py\ngrep -n 'DEFAULT_OPENHANDS_URL = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'def get_secret' /tmp/slack-monitor-build/main.py\ngrep -n 'def _state_file_path' /tmp/slack-monitor-build/main.py\ngrep -n 'def create_conversation' /tmp/slack-monitor-build/main.py\n```\n\nIf any of those checks fail, stop and re-copy the template instead of trying to\nrepair a hand-written variant.\n\n### Step 4 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/slack-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\nIf the upload fails with a size error, the tarball must be under 1 MB.\n`main.py` is under 15 KB so this should never trigger.\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Slack Channel Monitor\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"* * * * *\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nA 55-second timeout keeps runs well within the 60-second cron window.\n\nRecord the returned `id` - share it with the user as confirmation.\n\n### Step 6 - Confirm\n\nTell the user:\n\n> ✅ **Slack Channel Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Channels: `{channel list}`\n> - Trigger phrase: `{phrase}`\n> - Polling every minute via cron `* * * * *`\n> - State file: `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`\n>\n> Send a message containing `{phrase}` in any monitored channel to test it.\n> The bot will react with 👀 and reply with a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which runs **10 polling iterations** (every\n5 seconds) within the 55-second timeout window. Each iteration:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.\n3. **Fetches new messages:**\n - User token + `search:read` + > 1 channel → single `search.messages` call\n (searches for the trigger phrase across all channels).\n - Otherwise → one `conversations.history` call per channel.\n4. **Fetches thread replies** - one `conversations.replies` call per tracked thread.\n5. **Processes messages** in chronological order:\n - Skips messages already in `processed_ts` (dedup across the overlap window).\n - Skips bot messages and any `ts` in `bot_message_ts`.\n - Reply in a tracked thread (active **or** closed) → forwards to the existing\n conversation; re-opens closed conversations automatically.\n - Contains trigger phrase → 👀 reaction, create conversation, post link.\n - Thread replies: agent receives full thread history for context.\n - Root messages: agent receives the trigger text only.\n6. **Checks conversation statuses** - for each active conversation where\n `time.time() - last_activity > 15 s`:\n - If status is `idle`, `finished`, `error`, or `stuck` → fetch the agent's\n final response via `/api/conversations/{id}/agent_final_response` and post\n it to the Slack thread. Mark conversation `closed`.\n7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary\n races). If a conversation creation failed, pins `last_poll` further back to\n retry on the next iteration.\n8. **Saves state** (including `processed_ts`) and continues to the next iteration.\n9. After all iterations, fires the completion callback.\n\nDebug output is written to both stdout and a persistent log at:\n```\n{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log\n```\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/slack-api.md`** - Slack token types, required scopes, API\n endpoint reference, rate limits, and common error codes.\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n example file, and conversation lifecycle diagram.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the three\n constants at the top (`TRIGGER_PHRASE`, `CHANNEL_IDS`, `DEFAULT_OPENHANDS_URL`)\n before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't react to messages | Token missing or bot not in channel | Verify token with `auth.test`; `/invite @botname` |\n| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |\n| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |\n| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |\n| Conversation link 404 | `OPENHANDS_URL` points to wrong host | Set the `OPENHANDS_URL` secret to the correct base URL |\n| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |\n| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |\n| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |\n| Debug info needed | Need detailed per-message trace | Check `{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log` |"
|
|
390
402
|
},
|
|
391
403
|
{
|
|
392
404
|
"name": "slack-standup-digest",
|
|
@@ -66,7 +66,8 @@ The skill will:
|
|
|
66
66
|
1. Verify your Slack token is available
|
|
67
67
|
2. Resolve channel names to IDs
|
|
68
68
|
3. Confirm the trigger phrase (or use the default `@openhands`)
|
|
69
|
-
4. Generate and upload a customised automation script
|
|
69
|
+
4. Generate and upload a customised automation script by copying the template
|
|
70
|
+
and changing only the configuration constants
|
|
70
71
|
5. Create the automation with cron schedule `* * * * *`
|
|
71
72
|
|
|
72
73
|
## How It Works
|
|
@@ -119,8 +119,13 @@ Accepted values: any non-empty string unlikely to appear accidentally, e.g.
|
|
|
119
119
|
|
|
120
120
|
### Step 3 - Generate the automation script
|
|
121
121
|
|
|
122
|
-
Read `scripts/main.py` from this skill's directory
|
|
123
|
-
constant substitutions near the top of the file:
|
|
122
|
+
Read `scripts/main.py` from this skill's directory and **copy it verbatim**.
|
|
123
|
+
Apply exactly three constant substitutions near the top of the file:
|
|
124
|
+
|
|
125
|
+
> **Do not reimplement, simplify, or hand-write a replacement script.**
|
|
126
|
+
> The template already contains the correct secret-loading, state-path,
|
|
127
|
+
> conversation-creation, and context-forwarding logic. Only the three
|
|
128
|
+
> configuration constants below should change unless syntax validation fails.
|
|
124
129
|
|
|
125
130
|
| Placeholder | Replace with |
|
|
126
131
|
|---|---|
|
|
@@ -131,7 +136,8 @@ constant substitutions near the top of the file:
|
|
|
131
136
|
Write the customised script to a temporary directory:
|
|
132
137
|
```bash
|
|
133
138
|
mkdir -p /tmp/slack-monitor-build
|
|
134
|
-
#
|
|
139
|
+
# copy scripts/main.py to /tmp/slack-monitor-build/main.py
|
|
140
|
+
# then replace only the three constants above
|
|
135
141
|
```
|
|
136
142
|
|
|
137
143
|
Validate syntax before packaging:
|
|
@@ -139,7 +145,19 @@ Validate syntax before packaging:
|
|
|
139
145
|
python3 -m py_compile /tmp/slack-monitor-build/main.py && echo "Syntax OK"
|
|
140
146
|
```
|
|
141
147
|
|
|
142
|
-
|
|
148
|
+
Then run a quick integrity check to confirm the template structure is still
|
|
149
|
+
present and only the configuration block was customised:
|
|
150
|
+
```bash
|
|
151
|
+
grep -n 'TRIGGER_PHRASE = "' /tmp/slack-monitor-build/main.py
|
|
152
|
+
grep -n 'CHANNEL_IDS: list\[str\] =' /tmp/slack-monitor-build/main.py
|
|
153
|
+
grep -n 'DEFAULT_OPENHANDS_URL = "' /tmp/slack-monitor-build/main.py
|
|
154
|
+
grep -n 'def get_secret' /tmp/slack-monitor-build/main.py
|
|
155
|
+
grep -n 'def _state_file_path' /tmp/slack-monitor-build/main.py
|
|
156
|
+
grep -n 'def create_conversation' /tmp/slack-monitor-build/main.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
If any of those checks fail, stop and re-copy the template instead of trying to
|
|
160
|
+
repair a hand-written variant.
|
|
143
161
|
|
|
144
162
|
### Step 4 - Package and upload
|
|
145
163
|
|
|
@@ -198,6 +198,20 @@ class TestCollectNeededCommands:
|
|
|
198
198
|
assert spec.path.parent.name == "commands"
|
|
199
199
|
assert spec.path.suffix == ".md"
|
|
200
200
|
|
|
201
|
+
def test_colon_triggers_are_normalized_for_filenames(self, tmp_path, monkeypatch):
|
|
202
|
+
skill_dir = tmp_path / "skills" / "test-skill"
|
|
203
|
+
skill_dir.mkdir(parents=True)
|
|
204
|
+
(skill_dir / "SKILL.md").write_text(
|
|
205
|
+
"---\nname: test-skill\ndescription: Test\ntriggers:\n - /test:command\n---\nBody\n"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
monkeypatch.setattr("sync_extensions.SKILL_DIRS", [tmp_path / "skills"])
|
|
209
|
+
|
|
210
|
+
specs = collect_needed_commands()
|
|
211
|
+
assert [spec.path.relative_to(tmp_path).as_posix() for spec in specs] == [
|
|
212
|
+
"skills/test-skill/commands/test-command.md"
|
|
213
|
+
]
|
|
214
|
+
|
|
201
215
|
|
|
202
216
|
# ── load_marketplaces ────────────────────────────────────────────────
|
|
203
217
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/skills/github-repo-monitor/commands/{github-monitor:poll.md → github-monitor-poll.md}
RENAMED
|
File without changes
|
/package/skills/incident-retrospective/commands/{incident-retro:setup.md → incident-retro-setup.md}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/skills/slack-channel-monitor/commands/{slack-monitor:poll.md → slack-monitor-poll.md}
RENAMED
|
File without changes
|
/package/skills/slack-standup-digest/commands/{standup-digest:setup.md → standup-digest-setup.md}
RENAMED
|
File without changes
|