|
| 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