hub-launch 1.3.0 → 1.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.
@@ -0,0 +1,68 @@
1
+ import type { Config } from '../../types/index.js';
2
+ /**
3
+ * Options shared by publish/delete operations.
4
+ */
5
+ export interface FilePublishOptions {
6
+ /** Commit message to use for the change. */
7
+ commitMessage?: string;
8
+ /** Target branch to publish to. Defaults to `config.uploadBranch ?? 'main'`. */
9
+ branch?: string;
10
+ }
11
+ /**
12
+ * FilePublishService — commit a single repo-relative file to a remote branch
13
+ * (default `origin/main`) using a temporary detached worktree, without ever
14
+ * touching the user's current branch or working tree.
15
+ *
16
+ * This encapsulates the worktree publish/delete dance that was previously
17
+ * inlined in `executeUpload`. Both plan upload and `/hula-execute` skill
18
+ * publishing share this one tested implementation.
19
+ *
20
+ * The flow for each operation is:
21
+ * 1. validate the target branch (injection guard)
22
+ * 2. resolve an absolute worktree base path
23
+ * 3. create a detached worktree off `origin/<branch>`
24
+ * 4. write (publish) or remove (delete) the file at `<worktree>/<relativePath>`
25
+ * 5. stage + commit (`git add .` stages deletions too) when there are changes
26
+ * 6. push `HEAD:<branch>`
27
+ * 7. always remove the worktree in `finally`
28
+ */
29
+ export declare class FilePublishService {
30
+ private readonly config;
31
+ constructor(config: Config);
32
+ /**
33
+ * Default branch this service publishes to.
34
+ */
35
+ private defaultBranch;
36
+ /**
37
+ * Validate a branch value to prevent command injection in the git push
38
+ * refspec. Mirrors the guard in `executeUpload`.
39
+ */
40
+ private validateBranch;
41
+ /**
42
+ * Resolve the absolute worktree base path from config, identical to
43
+ * `executeUpload`'s behavior.
44
+ */
45
+ private resolveWorktreeBasePath;
46
+ /**
47
+ * Run `operation` inside a fresh detached worktree off `origin/<branch>`,
48
+ * commit (with `commitMessage`) if it produced changes, push to the branch,
49
+ * and always remove the worktree afterward.
50
+ */
51
+ private withWorktree;
52
+ /**
53
+ * Publish (create or update) a file on the target branch.
54
+ *
55
+ * @param relativePath - Repo-relative path of the file to write.
56
+ * @param content - The full content to write.
57
+ * @param opts - Optional commit message and branch override.
58
+ */
59
+ publishFile(relativePath: string, content: string, opts?: FilePublishOptions): Promise<void>;
60
+ /**
61
+ * Delete a file from the target branch.
62
+ *
63
+ * @param relativePath - Repo-relative path of the file to remove.
64
+ * @param opts - Optional commit message and branch override.
65
+ */
66
+ deleteFile(relativePath: string, opts?: FilePublishOptions): Promise<void>;
67
+ }
68
+ //# sourceMappingURL=FilePublishService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilePublishService.d.ts","sourceRoot":"","sources":["../../../src/services/git/FilePublishService.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAKnD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,MAAM,EAAE,MAAM;IAI1B;;OAEG;IACH,OAAO,CAAC,aAAa;IAIrB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAiBtB;;;OAGG;YACW,uBAAuB;IAQrC;;;;OAIG;YACW,YAAY;IAuD1B;;;;;;OAMG;IACG,WAAW,CACf,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;OAKG;IACG,UAAU,CACd,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC;CAWjB"}
@@ -0,0 +1,141 @@
1
+ import { dirname, isAbsolute, join, resolve } from 'path';
2
+ import { mkdirSync, rmSync, writeFileSync } from 'fs';
3
+ import { logger } from '../../utils/logger.js';
4
+ import { WorktreeService } from './WorktreeService.js';
5
+ import { getRepoInfo } from '../../utils/github-cli.js';
6
+ import { getRepoRoot } from '../../utils/shell.js';
7
+ /**
8
+ * FilePublishService — commit a single repo-relative file to a remote branch
9
+ * (default `origin/main`) using a temporary detached worktree, without ever
10
+ * touching the user's current branch or working tree.
11
+ *
12
+ * This encapsulates the worktree publish/delete dance that was previously
13
+ * inlined in `executeUpload`. Both plan upload and `/hula-execute` skill
14
+ * publishing share this one tested implementation.
15
+ *
16
+ * The flow for each operation is:
17
+ * 1. validate the target branch (injection guard)
18
+ * 2. resolve an absolute worktree base path
19
+ * 3. create a detached worktree off `origin/<branch>`
20
+ * 4. write (publish) or remove (delete) the file at `<worktree>/<relativePath>`
21
+ * 5. stage + commit (`git add .` stages deletions too) when there are changes
22
+ * 6. push `HEAD:<branch>`
23
+ * 7. always remove the worktree in `finally`
24
+ */
25
+ export class FilePublishService {
26
+ config;
27
+ constructor(config) {
28
+ this.config = config;
29
+ }
30
+ /**
31
+ * Default branch this service publishes to.
32
+ */
33
+ defaultBranch() {
34
+ return this.config.uploadBranch ?? 'main';
35
+ }
36
+ /**
37
+ * Validate a branch value to prevent command injection in the git push
38
+ * refspec. Mirrors the guard in `executeUpload`.
39
+ */
40
+ validateBranch(branch) {
41
+ if (branch.includes(';') ||
42
+ branch.includes('|') ||
43
+ branch.includes('&') ||
44
+ branch.includes('$') ||
45
+ branch.includes('`') ||
46
+ branch.includes('\\') ||
47
+ branch.includes('\n') ||
48
+ branch.includes('\r') ||
49
+ branch.includes('\0') ||
50
+ branch.trim() === '') {
51
+ throw new Error(`Invalid branch value: "${branch}"`);
52
+ }
53
+ }
54
+ /**
55
+ * Resolve the absolute worktree base path from config, identical to
56
+ * `executeUpload`'s behavior.
57
+ */
58
+ async resolveWorktreeBasePath() {
59
+ const rawBasePath = this.config.worktreeBasePath ?? '.hula-worktrees';
60
+ const repoRoot = await getRepoRoot();
61
+ return isAbsolute(rawBasePath)
62
+ ? rawBasePath
63
+ : resolve(repoRoot, rawBasePath);
64
+ }
65
+ /**
66
+ * Run `operation` inside a fresh detached worktree off `origin/<branch>`,
67
+ * commit (with `commitMessage`) if it produced changes, push to the branch,
68
+ * and always remove the worktree afterward.
69
+ */
70
+ async withWorktree(type, branch, commitMessage, operation) {
71
+ this.validateBranch(branch);
72
+ const worktreeBasePath = await this.resolveWorktreeBasePath();
73
+ const repoInfo = await getRepoInfo();
74
+ const worktreeService = new WorktreeService({
75
+ basePath: worktreeBasePath,
76
+ repoName: repoInfo.name,
77
+ });
78
+ // Timestamp-based identifier to avoid collisions between concurrent publishes.
79
+ const identifier = `${Date.now()}`;
80
+ const worktreePath = worktreeService.getWorktreeDir(type, identifier);
81
+ let worktreeCreated = false;
82
+ try {
83
+ await worktreeService.createWorktreeAt(worktreePath, `origin/${branch}`, true);
84
+ worktreeCreated = true;
85
+ operation(worktreePath);
86
+ const hasChanges = await worktreeService.hasChangesAt(worktreePath);
87
+ if (hasChanges) {
88
+ await worktreeService.commitChangesAt(worktreePath, commitMessage);
89
+ }
90
+ else {
91
+ logger.info('No changes to commit, proceeding to push...');
92
+ }
93
+ // For a detached HEAD worktree we push HEAD directly to the target branch.
94
+ await worktreeService.pushAt(worktreePath, `HEAD:${branch}`);
95
+ }
96
+ finally {
97
+ if (worktreeCreated) {
98
+ try {
99
+ await worktreeService.removeWorktreeAt(worktreePath);
100
+ }
101
+ catch (error) {
102
+ logger.warning(`Failed to remove worktree at ${worktreePath}: ${error instanceof Error ? error.message : String(error)}`);
103
+ logger.info(`You can manually remove it with: git worktree remove "${worktreePath}" --force`);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ /**
109
+ * Publish (create or update) a file on the target branch.
110
+ *
111
+ * @param relativePath - Repo-relative path of the file to write.
112
+ * @param content - The full content to write.
113
+ * @param opts - Optional commit message and branch override.
114
+ */
115
+ async publishFile(relativePath, content, opts) {
116
+ const branch = opts?.branch ?? this.defaultBranch();
117
+ const commitMessage = opts?.commitMessage ?? 'chore: publish file via hula';
118
+ await this.withWorktree('publish', branch, commitMessage, (worktreePath) => {
119
+ const filePath = join(worktreePath, relativePath);
120
+ mkdirSync(dirname(filePath), { recursive: true });
121
+ writeFileSync(filePath, content, 'utf-8');
122
+ });
123
+ }
124
+ /**
125
+ * Delete a file from the target branch.
126
+ *
127
+ * @param relativePath - Repo-relative path of the file to remove.
128
+ * @param opts - Optional commit message and branch override.
129
+ */
130
+ async deleteFile(relativePath, opts) {
131
+ const branch = opts?.branch ?? this.defaultBranch();
132
+ const commitMessage = opts?.commitMessage ?? 'chore: remove file via hula';
133
+ await this.withWorktree('publish', branch, commitMessage, (worktreePath) => {
134
+ const filePath = join(worktreePath, relativePath);
135
+ // `commitChangesAt` runs `git add .`, which stages deletions, so a plain
136
+ // filesystem remove is sufficient here.
137
+ rmSync(filePath, { force: true });
138
+ });
139
+ }
140
+ }
141
+ //# sourceMappingURL=FilePublishService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilePublishService.js","sourceRoot":"","sources":["../../../src/services/git/FilePublishService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env bash
2
+ # hula-execute-manage.sh — management surface for the /hula-execute skill
3
+ # Usage (exactly one action per call):
4
+ # bash .github/scripts/hula-execute-manage.sh --list
5
+ # bash .github/scripts/hula-execute-manage.sh --list-schedules
6
+ # bash .github/scripts/hula-execute-manage.sh --show <runId>
7
+ # bash .github/scripts/hula-execute-manage.sh --run-now <scheduleId>
8
+ # bash .github/scripts/hula-execute-manage.sh --cancel-schedule <scheduleId>
9
+ # bash .github/scripts/hula-execute-manage.sh --publish-skill <repo-relative-path>
10
+ # bash .github/scripts/hula-execute-manage.sh --delete-skill <repo-relative-path>
11
+ #
12
+ # Each invocation maps to the corresponding `hula execute …` call, captures its
13
+ # output, and emits a single structured JSON object to stdout; all other output
14
+ # goes to stderr.
15
+ #
16
+ # JSON contract (stdout):
17
+ # read verbs : {"status":"success","kind":"list|list-schedules|show","cliOutput":"…"}
18
+ # run-now : {"status":"success","kind":"run-now","runId":"…","cliOutput":"…"}
19
+ # cancel : {"status":"success","kind":"cancel-schedule","scheduleId":"…","cliOutput":"…"}
20
+ # publish-skill : {"status":"success","kind":"publish-skill","path":"…","cliOutput":"…"}
21
+ # delete-skill : {"status":"success","kind":"delete-skill","path":"…","cliOutput":"…"}
22
+ # failure : {"status":"error","code":<n>,"message":"…"}
23
+ #
24
+ # Exit codes: 0=success, 1=user error, 2=tool error
25
+ #
26
+ # Security: never prints secrets. Credentials are resolved inside the CLI from
27
+ # flags/config/env — they are NOT passed by this script. --publish-skill and
28
+ # --delete-skill reject path traversal and require a .hublaunch/skills/ path.
29
+
30
+ set -euo pipefail
31
+
32
+ # ── helpers ────────────────────────────────────────────────────────────────────
33
+
34
+ json_escape() {
35
+ if command -v jq &>/dev/null; then
36
+ # jq -Rs . reads stdin as a raw string and outputs a JSON string (with surrounding quotes).
37
+ # Strip the surrounding quotes; the escaped content is safe to embed in JSON.
38
+ printf '%s' "$1" | jq -Rs . | sed 's/^"//;s/"$//'
39
+ else
40
+ # Fallback: escape backslashes and double quotes, collapse control chars to spaces
41
+ printf '%s' "$1" \
42
+ | sed 's/\\/\\\\/g; s/"/\\"/g' \
43
+ | tr '\n\r\t' ' '
44
+ fi
45
+ }
46
+
47
+ die() {
48
+ local code="$1"; shift
49
+ local safe_msg
50
+ safe_msg=$(json_escape "$*")
51
+ printf '{"status":"error","code":%d,"message":"%s"}\n' "$code" "$safe_msg" >&1
52
+ exit "$code"
53
+ }
54
+
55
+ # Validate a skill path: must be repo-relative, under .hublaunch/skills/, no traversal.
56
+ validate_skill_path() {
57
+ local p="$1"
58
+ case "$p" in
59
+ /*) die 1 "Skill path must be repo-relative, not absolute: ${p}" ;;
60
+ esac
61
+ if printf '%s' "$p" | grep -q '\.\.'; then
62
+ die 1 "Invalid skill path (path traversal not allowed): ${p}"
63
+ fi
64
+ case "$p" in
65
+ .hublaunch/skills/*) : ;;
66
+ *) die 1 "Skill path must be under .hublaunch/skills/ — got ${p}" ;;
67
+ esac
68
+ }
69
+
70
+ # ── argument parsing ────────────────────────────────────────────────────────────
71
+ # Exactly one action flag is allowed per call.
72
+
73
+ ACTION=''
74
+ ARG=''
75
+
76
+ set_action() {
77
+ if [[ -n "$ACTION" ]]; then
78
+ die 1 "Provide exactly one action (got --$ACTION and $1)."
79
+ fi
80
+ ACTION="$1"
81
+ }
82
+
83
+ while [[ $# -gt 0 ]]; do
84
+ case "$1" in
85
+ --list) set_action 'list'; shift ;;
86
+ --list-schedules) set_action 'list-schedules'; shift ;;
87
+ --show) set_action 'show'; ARG="$2"; shift 2 ;;
88
+ --show=*) set_action 'show'; ARG="${1#--show=}"; shift ;;
89
+ --run-now) set_action 'run-now'; ARG="$2"; shift 2 ;;
90
+ --run-now=*) set_action 'run-now'; ARG="${1#--run-now=}"; shift ;;
91
+ --cancel-schedule) set_action 'cancel-schedule'; ARG="$2"; shift 2 ;;
92
+ --cancel-schedule=*) set_action 'cancel-schedule'; ARG="${1#--cancel-schedule=}"; shift ;;
93
+ --publish-skill) set_action 'publish-skill'; ARG="$2"; shift 2 ;;
94
+ --publish-skill=*) set_action 'publish-skill'; ARG="${1#--publish-skill=}"; shift ;;
95
+ --delete-skill) set_action 'delete-skill'; ARG="$2"; shift 2 ;;
96
+ --delete-skill=*) set_action 'delete-skill'; ARG="${1#--delete-skill=}"; shift ;;
97
+ *) die 1 "Unexpected argument: $1" ;;
98
+ esac
99
+ done
100
+
101
+ if [[ -z "$ACTION" ]]; then
102
+ die 1 "Usage: bash .github/scripts/hula-execute-manage.sh (--list | --list-schedules | --show <id> | --run-now <id> | --cancel-schedule <id> | --publish-skill <path> | --delete-skill <path>)"
103
+ fi
104
+
105
+ # Actions that require an argument.
106
+ case "$ACTION" in
107
+ show|run-now|cancel-schedule|publish-skill|delete-skill)
108
+ [[ -n "$ARG" ]] || die 1 "--$ACTION requires an argument." ;;
109
+ esac
110
+
111
+ # Path validation for skill file actions.
112
+ case "$ACTION" in
113
+ publish-skill|delete-skill) validate_skill_path "$ARG" ;;
114
+ esac
115
+
116
+ # ── Build and run the hula execute invocation ────────────────────────────────
117
+
118
+ CLI_ARGS=()
119
+ case "$ACTION" in
120
+ list) CLI_ARGS=(--list) ;;
121
+ list-schedules) CLI_ARGS=(--list-schedules) ;;
122
+ show) CLI_ARGS=(--show "$ARG") ;;
123
+ run-now) CLI_ARGS=(--run-now "$ARG") ;;
124
+ cancel-schedule) CLI_ARGS=(--cancel-schedule "$ARG") ;;
125
+ publish-skill) CLI_ARGS=(--publish-skill "$ARG") ;;
126
+ delete-skill) CLI_ARGS=(--delete-skill "$ARG") ;;
127
+ esac
128
+
129
+ printf '⚙️ hula execute --%s...\n' "$ACTION" >&2
130
+
131
+ OUTPUT=$(hula execute "${CLI_ARGS[@]}" 2>&1) || die 2 "Management action failed: $OUTPUT"
132
+
133
+ SAFE_OUTPUT=$(json_escape "$OUTPUT")
134
+
135
+ # ── Emit the structured JSON result ──────────────────────────────────────────
136
+
137
+ case "$ACTION" in
138
+ list|list-schedules|show)
139
+ printf '{"status":"success","kind":"%s","cliOutput":"%s"}\n' \
140
+ "$ACTION" "$SAFE_OUTPUT"
141
+ ;;
142
+ run-now)
143
+ # The CLI prints " ID: <id>" for the new run.
144
+ RUN_ID=$(printf '%s' "$OUTPUT" | grep -E '^\s*ID:' | head -1 | sed -E 's/^\s*ID:\s*//') || true
145
+ SAFE_ID=$(json_escape "$RUN_ID")
146
+ printf '{"status":"success","kind":"run-now","runId":"%s","cliOutput":"%s"}\n' \
147
+ "$SAFE_ID" "$SAFE_OUTPUT"
148
+ ;;
149
+ cancel-schedule)
150
+ SAFE_ID=$(json_escape "$ARG")
151
+ printf '{"status":"success","kind":"cancel-schedule","scheduleId":"%s","cliOutput":"%s"}\n' \
152
+ "$SAFE_ID" "$SAFE_OUTPUT"
153
+ ;;
154
+ publish-skill|delete-skill)
155
+ SAFE_PATH=$(json_escape "$ARG")
156
+ printf '{"status":"success","kind":"%s","path":"%s","cliOutput":"%s"}\n' \
157
+ "$ACTION" "$SAFE_PATH" "$SAFE_OUTPUT"
158
+ ;;
159
+ esac
@@ -0,0 +1,142 @@
1
+ # Execute Skill Creation Instructions
2
+
3
+ These instructions drive the **create-from-description** mode of the
4
+ `/hula-execute` skill: turning a plain-language description into a published
5
+ action file that `hula execute` can run or schedule.
6
+
7
+ The action file is plain instruction markdown that the hula-project server reads
8
+ from `origin/<uploadBranch>` (default `main`) at run time. It is **not** an Agent
9
+ Skill and is never registered as a slash command — it lives under
10
+ `.hublaunch/skills/`.
11
+
12
+ Follow these steps in order. Do **not** skip the question step, and do **not**
13
+ run `hula execute` before the file is successfully published.
14
+
15
+ ## Step 1: Ask clarifying questions first (then STOP)
16
+
17
+ Mirror the `/hula-plan` question-first behavior. Before writing anything, ask the
18
+ user the questions you cannot confidently answer from their description:
19
+
20
+ 1. **What should the action do?** The concrete task to perform on each run
21
+ (e.g. "remove unreachable code", "upgrade dependencies and fix breakages").
22
+ 2. **Target entry point** (optional): the file, directory, or URL the action
23
+ focuses on (e.g. `src/`). Maps to `--entry-point`.
24
+ 3. **Outcome type**: `pr` (open a pull request), `plan` (produce a plan), or
25
+ `feedback` (review/report only). Maps to `--outcome-type`. Default `pr`.
26
+ 4. **One-off or scheduled?** If recurring, get the schedule phrase (e.g. "every
27
+ night", "every Monday 9am").
28
+ 5. **Any constraints / scope limits** the run must respect (optional).
29
+
30
+ **STOP and wait for the answers.** Do not proceed to authoring until the user
31
+ responds. If anything is still ambiguous after the answers, ask a focused
32
+ follow-up round.
33
+
34
+ ## Step 2: Confirm the action name
35
+
36
+ Derive a default slug from the description: lowercase, hyphen-separated, no
37
+ spaces or punctuation (e.g. "remove unreachable code in src" →
38
+ `remove-unreachable-code`). Propose it to the user and let them override. Confirm
39
+ the final name before writing the file.
40
+
41
+ ## Step 3: Resolve the schedule (if recurring)
42
+
43
+ If the action is scheduled, translate the phrase to a 5-field cron expression
44
+ using the preset table in `SKILL.md`, then **echo the cron with a plain-English
45
+ readback and confirm it** before publishing/running. There is no server-side
46
+ cron validation, so the readback + confirmation is mandatory.
47
+
48
+ ## Step 4: Compute the filename and path
49
+
50
+ - Filename: `YYYY-MM-DD-HH:MM-<name-slug>.md`
51
+ - Use today's date and the current time on a 24-hour clock.
52
+ - `<name-slug>` is the confirmed, lowercase-hyphenated action name.
53
+ - Repo-relative path: `.hublaunch/skills/<filename>`
54
+ - Create the `.hublaunch/skills/` directory if it does not exist.
55
+
56
+ ## Step 5: Write the action file (free-form instruction markdown, NO frontmatter)
57
+
58
+ Use the `Write` tool to create the file at the computed path. The content MUST be
59
+ free-form instruction markdown with **no YAML frontmatter** (frontmatter would
60
+ make it look like an Agent Skill). Follow this template:
61
+
62
+ ```markdown
63
+ # <Action Title>
64
+
65
+ ## Goal
66
+ <1–3 sentences describing what this action should accomplish.>
67
+
68
+ ## Steps
69
+ 1. <step>
70
+ 2. <step>
71
+ 3. <step>
72
+
73
+ ## Constraints
74
+ - <optional: what must not change / scope limits>
75
+
76
+ ## Outcome
77
+ <What the run should produce, consistent with the chosen --outcome-type:
78
+ pr | plan | feedback.>
79
+ ```
80
+
81
+ Fill the template from the user's answers. Omit the `## Constraints` section if
82
+ the user gave no constraints.
83
+
84
+ ## Step 6: Publish to origin/main BEFORE running (mandatory ordering)
85
+
86
+ The server clones the repository and reads the action file from the default
87
+ branch at run time (and re-reads it on every scheduled fire). The file MUST be on
88
+ the branch before the run, so publish it first:
89
+
90
+ ```bash
91
+ bash .github/scripts/hula-execute-manage.sh --publish-skill .hublaunch/skills/<filename>
92
+ ```
93
+
94
+ Parse the single JSON object it prints:
95
+
96
+ - If `status` is `"error"`: display `❌ <message>` and **STOP**. Do **not** run
97
+ `hula execute` — the file is not on the branch, so the run would fail.
98
+ - If `status` is `"success"`: continue to Step 7.
99
+
100
+ ## Step 7: Run or schedule the action
101
+
102
+ Only after a successful publish, invoke the run wrapper with `--action-path`
103
+ pointing at the committed file:
104
+
105
+ ```bash
106
+ bash .github/scripts/hula-execute-run.sh --action-path .hublaunch/skills/<filename> [--entry-point <path>] [--outcome-type <type>] [--schedule "<cron>"]
107
+ ```
108
+
109
+ Pass only the flags you resolved. Quote the cron expression.
110
+
111
+ ## Step 8: Report the result
112
+
113
+ Parse the run wrapper's JSON (same contract as the normal run flow) and report:
114
+
115
+ - The **created file path** (`.hublaunch/skills/<filename>`) and that it was
116
+ published to `origin/<uploadBranch>`.
117
+ - The **run or schedule result**:
118
+ - One-off run → the Run ID and PR link (if any), plus
119
+ `hula execute --show <runId>` to check status.
120
+ - Schedule → the Schedule ID and cron expression, plus a note that you can
121
+ manage it with `/hula-execute list`, `/hula-execute run now <id>`,
122
+ `/hula-execute cancel <id>`, or `/hula-execute update <id> …`.
123
+
124
+ Example success report:
125
+
126
+ ```
127
+ ✅ Action created and published
128
+
129
+ 📄 **File**: .hublaunch/skills/2026-06-19-16:35-remove-unreachable-code.md (on origin/main)
130
+ 🔖 **Schedule ID**: sch_abc123
131
+ ⏰ **Cron**: 0 3 * * * (every day at 3:00 AM)
132
+
133
+ Manage it with: /hula-execute list · /hula-execute run now sch_abc123 · /hula-execute cancel sch_abc123
134
+ ```
135
+
136
+ ## Notes
137
+
138
+ - Never echo secrets. Credentials resolve inside the CLI.
139
+ - The action file is committed to `origin/<uploadBranch>` via a temporary
140
+ worktree — the user's current branch and working tree are never touched.
141
+ - If the publish succeeds but the run fails, the file remains on the branch; the
142
+ user can re-run it later with `/hula-execute .hublaunch/skills/<filename>`.