Skip to content

Commit db9d57c

Browse files
committed
ci and claude skills
1 parent 9c634fe commit db9d57c

3 files changed

Lines changed: 276 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
name: convert-gha
3+
description: Convert GitHub Actions workflows to Harmont pipelines. Use when the user has existing `.github/workflows/` YAML files and wants to migrate their CI to Harmont. Reads each workflow, maps GHA concepts to Harmont equivalents, explains differences, and delegates to the write-pipeline skill for the actual pipeline creation.
4+
---
5+
6+
Convert existing GitHub Actions workflows (`.github/workflows/*.yml` / `*.yaml`) into Harmont CI pipelines. This skill reads your GHA configuration, explains what maps directly and what changes, then uses the `write-pipeline` skill to produce the Harmont pipeline.
7+
8+
## When to use
9+
10+
- The user asks to "convert", "migrate", or "port" their GitHub Actions to Harmont
11+
- The user says "I have GHA workflows" and wants Harmont equivalents
12+
- The user asks "how do I replace GitHub Actions with Harmont"
13+
- `hm init` told the user about this skill after detecting `.github/workflows/`
14+
15+
## When NOT to use
16+
17+
- The user wants to write a Harmont pipeline from scratch — use the `write-pipeline` skill directly
18+
- The user wants to keep using GitHub Actions alongside Harmont (dual CI setup)
19+
- The user is debugging an existing Harmont pipeline — use the `validate-ci` skill
20+
21+
## Before you start
22+
23+
1. **Read every workflow file** in `.github/workflows/`:
24+
```bash
25+
find .github/workflows -name '*.yml' -o -name '*.yaml' | sort
26+
```
27+
Read each file. Understand what each workflow does, what triggers it, and what its jobs and steps accomplish.
28+
29+
2. **Fetch the Harmont patterns guide** — required before writing anything:
30+
```
31+
WebFetch https://docs.harmont.dev/pipeline-sdk/patterns.md
32+
```
33+
34+
## Procedure
35+
36+
1. **Inventory the GHA workflows.** For each `.yml` file, note:
37+
- Workflow name and trigger events (`on: push`, `on: pull_request`, etc.; note any `on: schedule` — see the mapping table for why scheduled triggers don't map to a local pipeline)
38+
- Each job: its name, `runs-on`, and what it does
39+
- Dependencies between jobs (`needs:`)
40+
- Services used (`services:`)
41+
- Secrets and environment variables referenced
42+
- Caching steps (`actions/cache`, `actions/setup-*` with built-in caching)
43+
- Artifacts (`actions/upload-artifact`, `actions/download-artifact`)
44+
- Matrix strategies
45+
46+
2. **Map GHA concepts to Harmont.** Present the user with a summary table covering what they have and how it translates:
47+
48+
| GHA concept | Harmont equivalent | Notes |
49+
|---|---|---|
50+
| `on: push` / `on: pull_request` | `push` / `pull_request` triggers | Direct mapping — same semantics |
51+
| `on: schedule` (cron) | No local DSL trigger | Scheduled pipelines are a Harmont Cloud concern, not a local pipeline trigger; omit the schedule or configure it in cloud. |
52+
| `jobs.<id>.steps` | Chain of toolchain calls or `sh()` | Each meaningful step becomes a Harmont step |
53+
| `jobs.<id>.needs` | `.fork()` for parallel, sequential chain for dependencies | Harmont DAG is implicit from chain structure |
54+
| `actions/cache` | **Not needed — caching is implicit in Harmont** | Harmont automatically caches build artifacts, dependency installs, and toolchain outputs between runs. Remove all cache steps. |
55+
| `actions/setup-*` (setup-node, setup-python, etc.) | Harmont toolchains (`hm.js`, `hm.python`, etc.) | Toolchains handle installation. Specify version via toolchain config. |
56+
| `actions/checkout` | **Not needed — source is always available** | Harmont automatically provides the source code to every step. |
57+
| `runs-on: ubuntu-latest` | `default_image: "ubuntu:24.04"` | Harmont runs steps in Docker containers |
58+
| `services:` (e.g., postgres) | Service containers in step config | Check docs for service container syntax |
59+
| `matrix:` | Multiple pipelines or parameterized steps | No direct matrix — may need separate pipeline definitions or `.fork()` |
60+
| `env:` / `secrets.*` | `env: {}` on pipeline or step | Secrets must be passed as environment variables |
61+
| `actions/upload-artifact` / `actions/download-artifact` | Step outputs and DAG dependencies | Harmont passes outputs between steps via the DAG |
62+
| `if:` conditionals | Pipeline-level logic (Python/TS) | Use the DSL's native control flow |
63+
64+
3. **Be honest about differences.** After presenting the mapping, explain:
65+
- **What's simpler:** Caching is implicit — no `actions/cache` boilerplate. No `actions/checkout` needed. Toolchains replace `actions/setup-*` with cleaner configuration.
66+
- **What's different:** Matrix strategies don't have a direct equivalent — you may need multiple pipeline definitions or `.fork()`. Service containers have different syntax. Complex `if:` conditionals become DSL-level control flow.
67+
- **What's a real gap:** Only mention a gap if functionality genuinely cannot be replicated. Do NOT invent problems — most GHA workflows map cleanly. Common real gaps: `on: schedule` cron triggers (the local DSL has only `push`/`pull_request`; scheduled runs are a Harmont Cloud concern), GHA marketplace actions that have no Harmont toolchain equivalent (use `sh()` with the underlying commands instead), GitHub-specific features like `github.event` context or `GITHUB_TOKEN` permissions.
68+
69+
4. **Delegate to the `write-pipeline` skill.** Once the user understands the mapping, invoke the `write-pipeline` skill to create the actual Harmont pipeline. Tell it:
70+
- What language/build system the project uses (detected from the GHA workflow)
71+
- The trigger configuration (mapped from GHA `on:`)
72+
- The step structure (mapped from GHA jobs and steps)
73+
- Any environment variables or services needed
74+
75+
5. **Validate the converted pipeline:**
76+
```bash
77+
hm render <pipeline-slug>
78+
```
79+
Then:
80+
```bash
81+
hm run
82+
```
83+
84+
6. **Summarize what changed.** After the pipeline works, tell the user:
85+
- Which GHA workflows were converted and what the Harmont pipeline covers
86+
- What was simplified (cache removal, checkout removal)
87+
- Any GHA features that were intentionally dropped and why
88+
- Remind them they can safely delete `.github/workflows/` once they're satisfied with the Harmont pipeline (but suggest keeping it until they've verified on a real push)
89+
90+
## Important
91+
92+
- **Read ALL GHA workflow files before starting.** Don't skip workflows — the user expects a complete migration.
93+
- **Do NOT fabricate differences.** Only flag a gap when something genuinely can't be done in Harmont. `actions/cache` removal is a simplification, not a gap.
94+
- **Delegate pipeline writing to the `write-pipeline` skill.** This skill handles analysis and mapping; `write-pipeline` handles the actual Harmont SDK usage and documentation fetching.
95+
- **One Harmont pipeline can replace multiple GHA workflows** if they share the same trigger. Consolidation is usually the right call.
96+
- The user's GHA workflows are the source of truth for what their CI does. Don't assume — read them.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
export const meta = {
2+
name: 'release-changelog',
3+
description: 'Investigate changes since the last release and write a new CHANGELOG.md release section',
4+
whenToUse: 'Cutting a release. Pass args {version, date?, lastTag?} — investigates every commit since the last tag and edits CHANGELOG.md in place.',
5+
phases: [
6+
{ title: 'Survey', detail: 'collect commits since the last release tag' },
7+
{ title: 'Investigate', detail: 'one agent per meaningful change → structured entry' },
8+
{ title: 'Write', detail: 'synthesize entries and edit CHANGELOG.md' },
9+
],
10+
}
11+
12+
// ---- release knobs ----------------------------------------------------
13+
// The `args` global is not delivered to scripts in this runtime, so the
14+
// release is parameterized by these consts. For a normal release leave them
15+
// as-is: the next version is auto-derived by bumping the requested segment of
16+
// the previous tag. For an arbitrary release, set VERSION_OVERRIDE.
17+
const VERSION_OVERRIDE = null // e.g. '0.1.0' to force an exact version; null = auto-bump
18+
const BUMP = 'patch' // 'patch' | 'minor' | 'major' — segment to increment when auto-bumping
19+
const LAST_TAG_OVERRIDE = null // e.g. 'v0.0.6'; null = `git describe --tags --abbrev=0`
20+
const lastTagHint = LAST_TAG_OVERRIDE
21+
22+
function bump(tag, which) {
23+
const m = String(tag).match(/(\d+)\.(\d+)\.(\d+)/)
24+
if (!m) throw new Error(`cannot parse a semver from last tag "${tag}"`)
25+
let [maj, min, pat] = [Number(m[1]), Number(m[2]), Number(m[3])]
26+
if (which === 'major') { maj += 1; min = 0; pat = 0 }
27+
else if (which === 'minor') { min += 1; pat = 0 }
28+
else { pat += 1 }
29+
return `${maj}.${min}.${pat}`
30+
}
31+
32+
// ---- schemas ----------------------------------------------------------
33+
const SURVEY_SCHEMA = {
34+
type: 'object',
35+
additionalProperties: false,
36+
required: ['lastTag', 'repoUrl', 'today', 'commits'],
37+
properties: {
38+
lastTag: { type: 'string', description: 'The previous release tag, e.g. v0.0.6' },
39+
repoUrl: { type: 'string', description: 'Canonical GitHub web URL, e.g. https://github.com/harmont-dev/harmont-cli' },
40+
today: { type: 'string', description: "Today's date as YYYY-MM-DD" },
41+
commits: {
42+
type: 'array',
43+
items: {
44+
type: 'object',
45+
additionalProperties: false,
46+
required: ['hash', 'author', 'subject', 'prNumber', 'meaningful'],
47+
properties: {
48+
hash: { type: 'string' },
49+
author: { type: 'string' },
50+
subject: { type: 'string' },
51+
prNumber: { type: ['integer', 'null'] },
52+
meaningful: { type: 'boolean', description: 'false for chore/CI/version-bump/merge noise that does not belong in a changelog' },
53+
},
54+
},
55+
},
56+
},
57+
}
58+
59+
const ENTRY_SCHEMA = {
60+
type: 'object',
61+
additionalProperties: false,
62+
required: ['skip', 'entries'],
63+
properties: {
64+
skip: { type: 'boolean', description: 'true if this commit is noise and should not appear in the changelog' },
65+
entries: {
66+
type: 'array',
67+
description: 'One or more changelog bullets derived from this change (a PR may touch multiple categories)',
68+
items: {
69+
type: 'object',
70+
additionalProperties: false,
71+
required: ['category', 'area', 'breaking', 'text', 'refLabel', 'refUrl', 'externalAuthor'],
72+
properties: {
73+
category: { type: 'string', enum: ['Changed', 'Added', 'Removed', 'Fixed'] },
74+
area: { type: ['string', 'null'], enum: ['CLI', 'DSL', 'SDK', null], description: 'Bold prefix; null if none fits' },
75+
breaking: { type: 'boolean' },
76+
text: { type: 'string', description: 'Bullet body only — no leading dash, no bold prefix, no ref link, no author' },
77+
refLabel: { type: 'string', description: 'e.g. "#140" for a PR, or a 7-char commit hash' },
78+
refUrl: { type: 'string', description: 'Full URL the ref points to (PR or commit)' },
79+
externalAuthor: { type: ['string', 'null'], description: 'Display name in trailing parens, ONLY for non-maintainer contributors; null otherwise' },
80+
},
81+
},
82+
},
83+
},
84+
}
85+
86+
const WRITE_SCHEMA = {
87+
type: 'object',
88+
additionalProperties: false,
89+
required: ['sectionMarkdown', 'refsAdded'],
90+
properties: {
91+
sectionMarkdown: { type: 'string', description: 'The full new release section as written into the file' },
92+
refsAdded: { type: 'array', items: { type: 'string' } },
93+
},
94+
}
95+
96+
// ---- Survey -----------------------------------------------------------
97+
phase('Survey')
98+
const survey = await agent(
99+
`You are surveying git history to prepare a CHANGELOG release.\n\n` +
100+
`Run these in the repo root:\n` +
101+
`1. Determine the previous release tag. ${lastTagHint ? `Use "${lastTagHint}".` : 'Run: git describe --tags --abbrev=0'}\n` +
102+
`2. git log <lastTag>..HEAD --format='%h | %an | %s'\n` +
103+
`3. git remote get-url origin (normalize ssh/scp form to an https web URL, strip trailing .git)\n` +
104+
`4. date +%Y-%m-%d → return as "today"\n\n` +
105+
`For each commit return hash, author, subject, prNumber (parse a trailing "(#NNN)" from the subject; null if none), ` +
106+
`and meaningful=false for changelog noise: "run ci", "auto-versioned ...", "bump version", merge commits, pure formatting/whitespace. ` +
107+
`Everything user- or developer-facing is meaningful=true.`,
108+
{ schema: SURVEY_SCHEMA, label: 'survey', phase: 'Survey' }
109+
)
110+
111+
const repoUrl = survey.repoUrl.replace(/\.git$/, '')
112+
const version = VERSION_OVERRIDE || bump(survey.lastTag, BUMP)
113+
const date = survey.today
114+
const meaningful = survey.commits.filter((c) => c.meaningful)
115+
log(`${survey.lastTag}${version} (${date}): ${survey.commits.length} commits, ${meaningful.length} meaningful`)
116+
117+
// ---- Investigate ------------------------------------------------------
118+
phase('Investigate')
119+
const investigated = await parallel(
120+
meaningful.map((c) => () =>
121+
agent(
122+
`Investigate one change and produce changelog bullet(s) in Keep-a-Changelog style.\n\n` +
123+
`Commit: ${c.hash}\nSubject: ${c.subject}\nAuthor: ${c.author}\nPR: ${c.prNumber ? '#' + c.prNumber : 'none'}\n\n` +
124+
`Read the actual change:\n` +
125+
`- git show --stat ${c.hash}\n` +
126+
(c.prNumber ? `- gh pr view ${c.prNumber} --json title,author,body (use the PR body for the WHY)\n` : '') +
127+
`\nClassify into category (Changed/Added/Removed/Fixed), area (CLI/DSL/SDK or null), and breaking (bool). ` +
128+
`Write each bullet as a tight, user-facing sentence describing the OUTCOME, not the implementation. Past where it helps, name the new command/flag/API. ` +
129+
`Split into multiple entries only when a change genuinely spans categories (e.g. a feature that also removes dead behavior).\n\n` +
130+
`refLabel/refUrl: prefer the PR (${c.prNumber ? `#${c.prNumber}${repoUrl}/pull/${c.prNumber}` : `none — use commit ${c.hash}${repoUrl}/commit/${c.hash}`}).\n` +
131+
`externalAuthor: the contributor's display name ONLY if they are NOT the repo maintainer (maintainer = the dominant committer); else null.\n` +
132+
`If on reflection this change is pure noise, set skip=true with an empty entries array.`,
133+
{ schema: ENTRY_SCHEMA, label: c.prNumber ? `#${c.prNumber}` : c.hash, phase: 'Investigate' }
134+
)
135+
)
136+
)
137+
138+
const allEntries = investigated
139+
.filter(Boolean)
140+
.filter((r) => !r.skip)
141+
.flatMap((r) => r.entries)
142+
143+
if (allEntries.length === 0) {
144+
log('No meaningful changes found — nothing to release.')
145+
return { version, lastTag: survey.lastTag, entries: [], note: 'empty' }
146+
}
147+
148+
// ---- Write ------------------------------------------------------------
149+
phase('Write')
150+
const ORDER = ['Changed', 'Added', 'Removed', 'Fixed']
151+
const grouped = ORDER.map((cat) => ({ cat, items: allEntries.filter((e) => e.category === cat) })).filter((g) => g.items.length)
152+
const entriesJson = JSON.stringify(grouped, null, 2)
153+
154+
const write = await agent(
155+
`Edit CHANGELOG.md to cut release ${version}${date ? ` dated ${date}` : ''}. Match the file's EXISTING formatting exactly — read it first.\n\n` +
156+
`Entries to write, already grouped and ordered (Changed, Added, Removed, Fixed):\n${entriesJson}\n\n` +
157+
`Repo web URL: ${repoUrl}\n\n` +
158+
`Rules, derived from the existing entries in the file:\n` +
159+
`- Each bullet: "- " then, if breaking, "**Breaking:** ", then if area set "**<AREA>:** ", then the text, then " (${'[refLabel][ref<n>]'} link)" as "([${'<refLabel>'}][${'<linkid>'}])", then if externalAuthor " (<name>)".\n` +
160+
` Concretely a PR ref "#140" renders inline as "([#140][pr140])" and needs a link def "[pr140]: ${repoUrl}/pull/140". A commit ref like "1bf727e" renders "([\`1bf727e\`][c1bf727e])" with def "[c1bf727e]: ${repoUrl}/commit/1bf727e".\n` +
161+
`- Insert a new "## [${version}] - ${date || '<DATE>'}" section immediately BELOW the "## [Unreleased]" heading, leaving "## [Unreleased]" present and empty.\n` +
162+
`- Under it emit each non-empty category as "### <Category>" with its bullets, in the given order.\n` +
163+
`- Append the new link-reference definitions to the link-def block at the BOTTOM of the file, next to the existing ones. Do not duplicate a def that already exists.\n` +
164+
`- Do NOT touch existing released sections.\n\n` +
165+
`Apply the edit with the Edit tool, then return the new section markdown and the list of link defs you added.`,
166+
{ schema: WRITE_SCHEMA, label: 'write-changelog', phase: 'Write' }
167+
)
168+
169+
return {
170+
version,
171+
date,
172+
lastTag: survey.lastTag,
173+
entryCount: allEntries.length,
174+
section: write.sectionMarkdown,
175+
refsAdded: write.refsAdded,
176+
}

.hm/config.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
backend = "cloud"
2+
3+
[cloud]
4+
org = "marko-harmont-dev"

0 commit comments

Comments
 (0)