Skip to content

Commit f8e51b9

Browse files
committed
chore(skills): add release-widget skill
Automates widget/module release pipeline: version bump, GitHub draft release, OSS clearance SBOM, Marketplace publish. Sharing for team feedback before promoting out of private trial.
1 parent a5f06c0 commit f8e51b9

1 file changed

Lines changed: 268 additions & 0 deletions

File tree

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
---
2+
name: release-widget
3+
description: Use when releasing a standalone Mendix widget or module from the web-widgets monorepo — version bump through Marketplace publish. Guides module-vs-standalone detection, prereqs, changelog-driven version selection, and drives the release pipeline directly (git/gh/pnpm) instead of a manual wizard.
4+
---
5+
6+
# Release Widget
7+
8+
## Overview
9+
10+
Releases a widget (or the module wrapping it) from this monorepo: version bump → GitHub draft release → OSS clearance → Marketplace publish.
11+
12+
**Autonomy carve-out (this skill only):** unlike the repo's default stance of never pushing/publishing without asking, this skill is pre-authorized to run `git push`, `gh workflow run`, `gh pr merge`, and `gh release edit --draft=false` (publish) directly, without pausing for confirmation on each one — because a human already invoked this skill specifically to run a release. This does **not** extend to destructive rollback (deleting releases/tags/branches) or anything outside this skill's scope.
13+
14+
**State is re-derived every run.** There is no persisted release-state file. Each invocation re-checks git/GitHub/Jira/Marketplace reality from scratch — safe to stop and resume this skill across sessions (e.g. while waiting days for OSS clearance).
15+
16+
## Prerequisites
17+
18+
Ask only if not already known:
19+
20+
1. **Widget name** — e.g. `combobox-web`. If not given, ask: "Which widget are you releasing?"
21+
22+
Everything else (module detection, environment prereqs, version state) — check automatically in Phase 0, don't ask.
23+
24+
## Workflow
25+
26+
### Phase 0 — Detect release target
27+
28+
Read the widget's marketplace info directly via the repo's own helper (don't grep — the schema is the source of truth):
29+
30+
```bash
31+
cd automation/utils
32+
pnpm exec ts-node -e "
33+
import { getPackageInfo } from './src/package-info';
34+
getPackageInfo('$(pwd)/../../packages/pluggableWidgets/<widget>').then(info => {
35+
console.log(JSON.stringify({ appNumber: info.marketplace.appNumber ?? null, appName: info.marketplace.appName, version: info.version.format(), name: info.name }));
36+
}).catch(e => console.error('ERR', e.message));
37+
"
38+
```
39+
40+
Use an **absolute path** to the widget dir — the script resolves `import()` relative to its own module location, not cwd.
41+
42+
- `appNumber` is a positive number → **standalone widget release**. `$RELEASE_PATH = packages/pluggableWidgets/<widget>`.
43+
- `appNumber` is `null`/absent → widget is module-wrapped, not published on its own. Find the owning module:
44+
```bash
45+
grep -l "\"@mendix/<widget>\"" packages/modules/*/package.json
46+
```
47+
That module's directory is `$RELEASE_PATH`. Tell the user which module wraps it. If no module found, stop — this is a misconfigured package, not something to guess through.
48+
49+
### Phase 1 — Prerequisite check
50+
51+
Run once, report all results together (don't ask one at a time):
52+
53+
```bash
54+
echo "== SBOM jar =="; ls ~/SBOM_Generator.jar 2>&1
55+
echo "== gh auth =="; gh auth status 2>&1
56+
echo "== JIRA token =="; [[ -n "$JIRA_API_TOKEN" ]] && echo set || echo missing
57+
echo "== commitlint =="; ls node_modules/.bin/commitlint 2>/dev/null || echo missing
58+
echo "== git branch/status =="; git branch --show-current; git status --short
59+
echo "== main sync =="; git fetch origin main --quiet; git rev-list HEAD..origin/main --count; git rev-list origin/main..HEAD --count
60+
```
61+
62+
If not on `main` or not in sync — fix it yourself (`git checkout main`, `git merge --ff-only origin/main`) rather than asking, unless `main` has diverged from `origin/main` (both ahead and behind) — that needs a human decision, stop and ask.
63+
64+
If commitlint or the SBOM jar is missing, tell the user exactly what's missing and how to fix it (`pnpm install`, or where to get `SBOM_Generator.jar`) — don't proceed past a missing prereq.
65+
66+
### Phase 2 — Version selection
67+
68+
Read the unreleased changelog and current version:
69+
70+
```bash
71+
sed -n '/## \[Unreleased\]/,/## \[/p' $RELEASE_PATH/CHANGELOG.md | head -40
72+
grep '"version"' $RELEASE_PATH/package.json | head -1
73+
```
74+
75+
Summarize the unreleased entries by type (Fixed/Added/Changed/Breaking changes) and propose a semver bump:
76+
77+
- Any "Breaking changes" section present → propose **major**, but flag it as a recommendation, not a mandate.
78+
- Only "Added" → propose **minor**.
79+
- Only "Fixed" → propose **patch**.
80+
81+
Ask the user to confirm or override — this is the one decision in the pipeline that's inherently a judgment call, always ask. If the user picks something inconsistent with changelog content (e.g. patch despite a breaking-changes note), flag the mismatch once, then respect their choice.
82+
83+
### Phase 3 — Version bump + release branch (autonomous)
84+
85+
Compute the next version and bump both files using the repo's real version-math helper (not a reimplementation):
86+
87+
```bash
88+
cd automation/utils
89+
pnpm exec ts-node -e "
90+
import { getNewVersion, bumpPackageJson, bumpXml } from './src/bump-version';
91+
const next = getNewVersion('<patch|minor|major>', '<currentVersion>');
92+
console.log('next:', next);
93+
bumpPackageJson('$(pwd)/../../$RELEASE_PATH', next);
94+
bumpXml('$(pwd)/../../$RELEASE_PATH', next).catch(e => console.error('no package.xml (module?):', e.message));
95+
"
96+
```
97+
98+
Then, directly (no wizard):
99+
100+
```bash
101+
git checkout -b tmp/<widget-or-module>-v<version>
102+
git add $RELEASE_PATH
103+
git commit -m "chore(<widget-or-module>): bump version to <version>"
104+
git push -u origin tmp/<widget-or-module>-v<version>
105+
```
106+
107+
If the branch already exists locally or on remote, stop and ask — don't guess a random suffix, that was a wizard fallback for unattended use, not something to do silently on someone's behalf.
108+
109+
**Jira version** (skip cleanly if `JIRA_API_TOKEN` missing or the API call fails — this has historically 404'd transiently and is not a blocker):
110+
111+
```bash
112+
cd automation/utils
113+
pnpm exec ts-node -e "
114+
import { Jira } from './src/jira';
115+
const jira = new Jira(process.env.JIRA_PROJECT_KEY ?? 'WC', process.env.JIRA_BASE_URL ?? 'https://mendix.atlassian.net', process.env.JIRA_API_TOKEN!);
116+
jira.initializeProjectData().then(() => jira.createVersion('<widget-or-module>-v<version>')).then(v => console.log('created:', v.name)).catch(e => console.error('skip:', e.message));
117+
"
118+
```
119+
120+
Trigger the GitHub release workflow directly:
121+
122+
```bash
123+
gh workflow run "CreateGitHubRelease.yml" --ref "tmp/<widget-or-module>-v<version>" -f package=<npm-package-name>
124+
```
125+
126+
Poll for completion:
127+
128+
```bash
129+
gh run list --workflow="CreateGitHubRelease.yml" -L 1 --json databaseId,status,conclusion
130+
gh run view <databaseId> --json status,conclusion,url
131+
```
132+
133+
Wait (re-poll, don't ask the user to check) until `status == completed`. Report the conclusion and the draft release URL.
134+
135+
### Phase 4 — OSS clearance SBOM (autonomous prep, manual submission)
136+
137+
Download the MPK from the draft release and generate the SBOM zip directly — don't use the interactive `oss-clearance` wizard, call the same underlying helpers:
138+
139+
```bash
140+
cd automation/utils
141+
pnpm exec ts-node -e "
142+
import { gh } from './src/github';
143+
import { createSBomGeneratorFolderStructure, generateSBomArtifactsInFolder } from './src/oss-clearance';
144+
import { join } from 'path';
145+
import { homedir } from 'os';
146+
147+
async function main() {
148+
await gh.ensureAuth();
149+
const releaseId = await gh.getReleaseIdByReleaseTag('<widget-or-module>-v<version>');
150+
const assets = await gh.listReleaseAssets(releaseId!);
151+
const mpk = assets.find(a => a.name.endsWith('.mpk'));
152+
if (!mpk) throw new Error('no MPK asset found');
153+
const releaseName = '<AppName> v<version>'; // e.g. 'Combo box v2.9.0'
154+
const [tmpFolder, downloadPath] = await createSBomGeneratorFolderStructure(releaseName);
155+
await gh.downloadReleaseAsset(mpk.id, downloadPath);
156+
const finalPath = join(homedir(), 'Downloads', \`\${releaseName} [pending-hash].zip\`);
157+
await generateSBomArtifactsInFolder(tmpFolder, join(homedir(), 'SBOM_Generator.jar'), releaseName, finalPath);
158+
console.log('SBOM zip:', finalPath);
159+
}
160+
main().catch(e => { console.error(e); process.exit(1); });
161+
"
162+
```
163+
164+
**Submission is manual** — the OSS clearance request now goes through the OSS clearance portal (a Mendix app, log in with Mendix credentials), not email. Tell the user:
165+
166+
- The zip is ready at the printed path.
167+
- Ask them to submit it via the OSS clearance portal (they know the URL/login flow; don't guess or fetch a URL for this).
168+
- Draft the request content (widget/module name, version, draft release URL, one-line summary of changes from the changelog) so they can paste it into the portal.
169+
170+
Then ask: "Submitted? Waiting on OSS team reply (a READMEOSS HTML file)." This wait is inherently unbounded (days) — the skill can be safely re-invoked later; Phase 0–4 will just confirm state is unchanged and skip straight back here.
171+
172+
### Phase 5 — Include OSS Readme (autonomous once file is provided)
173+
174+
Once the user has the READMEOSS HTML file (ask where it was saved — default search locations are `~/Downloads` and `~/Documents`):
175+
176+
```bash
177+
cd automation/utils
178+
pnpm exec ts-node -e "
179+
import { gh } from './src/github';
180+
import { findAllReadmeOssLocally, getRecommendedReadmeOss } from './src/oss-clearance';
181+
import { basename } from 'path';
182+
183+
async function main() {
184+
await gh.ensureAuth();
185+
const releaseId = await gh.getReleaseIdByReleaseTag('<widget-or-module>-v<version>');
186+
const readmes = findAllReadmeOssLocally();
187+
const recommended = getRecommendedReadmeOss('<AppName> v<version>', readmes);
188+
if (!recommended) throw new Error('no matching READMEOSS found in Downloads/Documents — ask the user for the path');
189+
const asset = await gh.uploadReleaseAsset(releaseId!, recommended, basename(recommended));
190+
console.log('uploaded:', asset.name);
191+
}
192+
main().catch(e => { console.error(e); process.exit(1); });
193+
"
194+
```
195+
196+
### Phase 6 — Asset gate + publish (GATE — do not skip)
197+
198+
**Before ever publishing, verify both assets are present:**
199+
200+
```bash
201+
gh release view <widget-or-module>-v<version> --json assets --jq '.assets[].name'
202+
```
203+
204+
Require: exactly one `.mpk` file AND one `*READMEOSS*.html` file. If either is missing, **refuse to publish** and tell the user what's missing. If the user explicitly says to publish anyway, comply but state clearly that this is an unverified publish (no asset-gate passed).
205+
206+
Once the gate passes, publish directly (carve-out applies — this is a forward release action):
207+
208+
```bash
209+
gh release edit <widget-or-module>-v<version> --draft=false
210+
```
211+
212+
Publishing triggers `PublishMarketplace.yml` automatically (on `release: published`). Do not also manually re-run the marketplace-publish workflow for the same tag unless the automatic run actually failed — see Phase 7 for how to tell the difference.
213+
214+
### Phase 7 — Marketplace publish verification
215+
216+
```bash
217+
gh run list --workflow="Publishes a package to marketplace" -L 5 --json databaseId,status,conclusion,headBranch,createdAt
218+
```
219+
220+
Find the run matching this tag/branch.
221+
222+
- `conclusion: success` → done. Merge the changelog PR (this repo's automation should trigger this, but verify):
223+
224+
```bash
225+
gh pr list --head "tmp/<widget-or-module>-v<version>" --json number,state
226+
```
227+
228+
If still open and unmerged after a successful publish, that's unexpected — check whether the workflow's own `merge-changelogs-pr` step ran, don't just merge it yourself without checking why it didn't auto-merge.
229+
230+
- `conclusion: failure`**before assuming stuck-draft or escalating, check history first**:
231+
```bash
232+
gh run view <databaseId> --log-failed | grep -A3 "Response status Code"
233+
```
234+
If it's a `409` on `POST .../packages/<appNumber>/versions`:
235+
1. Check whether an **earlier run for this exact tag already succeeded**: `gh run list --workflow="Publishes a package to marketplace" --json databaseId,status,conclusion,createdAt,headBranch` filtered to this tag. If a prior run for the same tag succeeded, the 409 on this run means **the version is already published** — not a real failure. Report that, don't escalate, don't retry, don't teardown.
236+
2. If no prior success exists for this tag: this is the same failure mode from the last incident (real backend conflict, not caused by our script — `createDraft()` has no idempotency check, so a 409 here is either a genuine stuck server-side state or a double-trigger — check `gh run list` for more than one run created within seconds of each other for the same tag, which would indicate a double-trigger).
237+
3. Only after ruling out (1) and confirming a real conflict: report the exact escalation details (appNumber, tag, endpoint, error) and ask the user to check the Marketplace UI for stuck drafts. Marketplace UI actions are manual — give exact navigation steps (Marketplace → package page → Manage Versions → search version → delete draft), the user executes and reports back.
238+
4. Do not blindly `gh run rerun` more than once without new information — 3 identical reruns with no state change, as happened previously, wastes time. Rerun once after the user confirms they've taken an action (deleted a draft, etc.), not speculatively.
239+
240+
### Phase 8 — Rollback (human-gated, always — carve-out does not apply here)
241+
242+
If the user wants to undo a release attempt, list the exact teardown commands and **wait for explicit confirmation before running any of them**, regardless of how far the carve-out extends elsewhere in this skill:
243+
244+
```bash
245+
gh release view <tag> --json tagName,isDraft,isPrerelease # confirm current state first
246+
gh pr list --head "tmp/<widget-or-module>-v<version>" --json number,url,state
247+
```
248+
249+
Teardown list (present all, confirm once, then execute):
250+
251+
1. `gh release delete <tag> --yes` (only if it exists)
252+
2. `git push origin --delete <tag>` (remote tag)
253+
3. `git push origin --delete tmp/<widget-or-module>-v<version>` (auto-closes any open PR)
254+
4. Jira version: cannot be deleted via available tooling — tell the user to check `<widget-or-module>-v<version>` in Jira manually.
255+
5. Marketplace: if a draft/version was created there, that's manual — tell the user to check.
256+
257+
## Common Mistakes
258+
259+
- **Using relative paths in the `ts-node -e` snippets**`import()` inside `automation/utils/src/*` resolves relative to that module's own location, not your cwd. Always pass absolute paths to widget/module directories.
260+
- **Treating `appNumber` presence via grep instead of reading the schema** — a module-wrapped widget's package.json simply omits the `marketplace.appNumber` key; check for `null`/undefined via `getPackageInfo`, don't grep for the string `"appNumber"` (unreliable — the field can exist with value `-1` too, which also means "not independently published").
261+
- **Publishing before the asset gate passes** — this is the exact mistake pattern that caused the 409 double-trigger risk. Never call `gh release edit --draft=false` without first confirming both MPK and READMEOSS assets are attached.
262+
- **Escalating a 409 without checking run history first** — many past "failures" are actually the second of two triggers for an already-successful publish. Always check `gh run list` history for the tag before treating a 409 as a real incident.
263+
- **Retrying `gh run rerun` speculatively** — reruns without new information (e.g., a deleted draft) just reproduce the same failure. Only rerun after the user confirms they changed something.
264+
- **Running rollback commands without the explicit go-ahead** — this is the one phase where the autonomy carve-out does not apply. Always list and wait for confirmation.
265+
266+
## Reference Files
267+
268+
None yet — this skill is new (rebuilt from lost prior version + 2026-07 incident history) and running in a private trial (`.agents/skills/`, untracked) before being proposed for the shared skill set. If patterns emerge from real runs (new failure modes, widget-specific quirks), add them here rather than growing the phases above indefinitely.

0 commit comments

Comments
 (0)