Skip to content

Commit d5afa8b

Browse files
javiAIJavierclaude
authored
feat(f4): marketplace.json + release.yml + bump 0.1.0 (cierre Fase F) (#28)
* test(f4): RED — marketplace.json + release.yml + plugin.json version pin Fase 0 (kickoff) + Fase 1 (RED tests) for feat/f4-marketplace-public-repo. Scope (per Fase -1 ratificada con 8 ajustes del usuario): - Aterrizar infra local del marketplace + release flow sin depender de que javiAI/pos-marketplace exista todavía (A1.b). - .claude-plugin/marketplace.json con schema oficial Claude Code: top-level {name, owner, plugins}; owner.name; plugin {name, source} con source.{source=github, repo, ref="v"+version}. - .github/workflows/release.yml trigger tag:v*, jobs version-match / selftest / build-bundle / publish-release / mirror-marketplace (mirror condicional/skippable hasta repo público). - Bump plugin.json.version 0.0.1 → 0.1.0 (primer release público). Archivos en este commit (RED tests, expected failures): - bin/tests/test_marketplace_json_schema.py (12 tests) - bin/tests/test_release_workflow_smoke.py (6 tests) - bin/tests/test_plugin_json_version_bump.py (3 tests) Tests verifican: - marketplace.json schema oficial mínimo (top-level + owner + plugin) - plugin name/version/ref sync entre marketplace ↔ plugin.json - release.yml trigger v*, jobs esperados, publish-release.needs ⊇ {version-match, selftest, build-bundle}, mirror-marketplace conditional/skippable - plugin.json.version pin = "0.1.0" Estado RED actual: 19 failed + 12 passed (12 = F3 baseline 9 + 3 plugin.json existe/parses). Sin regresión en F3. Diferidos en F4 (regla #7 CLAUDE.md): - audit.yml nightly (sin consumer hoy; rama propia post-F4). - /pos:pr-description, /pos:release skills (sin repetición demostrada). - CHANGELOG.md enforced (auto-generated from git log entre tags). - refactor/template-policy-d5b-migration (drift independiente). - Fase G (Knowledge Plane). GREEN impl + docs (RELEASE.md/ARCHITECTURE/ci-cd/MASTER_PLAN/ROADMAP/ HANDOFF) entran en commits siguientes dentro de esta rama. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(f4): GREEN — marketplace.json + release.yml + plugin.json 0.1.0 Fase 2 (GREEN) — flippea los 19 RED del commit previo. .claude-plugin/marketplace.json (NEW): - Schema oficial Claude Code marketplace. - top-level: name="pos-marketplace", owner.name="javiAI", plugins[]. - plugins[0]: name=pos, source={source:github, repo:javiAI/ project-operating-system, ref:v0.1.0}, version=0.1.0. - metadata.{description, version} para humans. .claude-plugin/plugin.json: - version 0.0.1 → 0.1.0 (primer release público; pre-1.0). - Single source of truth; tag git debe ser v${version}. .github/workflows/release.yml (NEW): - Trigger: push tags v*. - Jobs: - version-match: assert plugin.json.version == ${tag#v}. - selftest: pytest bin/tests -q (reusa contrato F3). - build-bundle: tar.gz curated plugin-only (.claude-plugin/, .claude/skills/, .claude/rules/, hooks/, agents/, policy.yaml, bin/pos-selftest.sh, bin/_selftest.py, docs/RELEASE.md). Excluye generator/, tools/, templates/, questionnaire/. - publish-release: needs [version-match, selftest, build-bundle]; gh release create con bundle como asset. - mirror-marketplace: condicional vía vars.POS_MARKETPLACE_REPO; si vacío skippea sin fallar release. Abre PR contra repo público cuando esté configurado. - Actions pinneadas por SHA (ci-cd.md regla #2). - permissions.contents=write para gh release create. Tests post-GREEN: 21 passed (12 marketplace + 6 release.yml + 3 plugin version), suite total 644 passed + 1 skipped (skip D5 intencional F3). Sin regresión. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(f4): sync — RELEASE runbook + ARCH §13 + ci-cd + ROADMAP/HANDOFF/MASTER_PLAN Fase N+3 — docs-sync dentro de la rama (CLAUDE.md regla #2, docs.md § Docs-sync en cada rama). docs/RELEASE.md (NEW): - Runbook user-facing de versionado + bundle + flujo + recovery. - Contrato de versionado: plugin.json.version source of truth; tag = v${version}; marketplace.json.source.ref espeja. - Bundle scope plugin-only curated (incluye/excluye explícitos). - Flujo en 5 pasos: bump → tag → workflow → verify → recovery. - Activación del mirror cuando exista repo público (3 pasos: crear repo + gh variable set POS_MARKETPLACE_REPO + gh secret set POS_MARKETPLACE_TOKEN). - Instalación user-facing (/plugin marketplace add + /plugin install pos). - Diferidos enumerados. docs/ARCHITECTURE.md § 13 (Marketplace + Release flow): - Reescrita de placeholder de 6 líneas a sub-sección completa. - Manifest, source of truth de versión, jobs del workflow, bundle scope curated, deferral del repo público, determinismo del flujo, instalación user-facing, deferrals. .claude/rules/ci-cd.md: - Bullet release.yml promovido de "Diferidos" a "Aterrizado" (entregado en F4). - Nuevo H3 "### Job release (entregado en F4)" con scope completo (5 jobs + bundle curated + source of truth). ROADMAP.md: - Tabla: F4 marcada ✅ (PR pendiente). - Nueva sección § feat/f4-marketplace-public-repo en Progreso Fase F: scope, entregables, decisiones Fase -1 (A1.b..A8), contrato fijado, carry-overs, criterio de salida (665 passed + 1 skipped). HANDOFF.md: - §1 Snapshot: rama actual F4 (entrega + suite update). - §9 Próxima rama: Fase F cerrada; carry-overs (template-policy d5b migration, marketplace activación, skills diferidas, audit.yml). - §22 nuevo: Estado F4 con entregables + contrato + decisiones + carry-overs (paralelo a §19 F1, §20 F2, §21 F3). MASTER_PLAN.md § Rama F4: - Expandida de 3 líneas a sección completa: scope realizado, archivos entregados con detalle por path, decisiones Fase -1 (A1.b..A8), contexto leído, criterio de salida, carry-overs. Simplify pre-commit: - Recortados 3 bullets de "Ajustes durante implementación" (heredoc syntax glitch + rtk wrapper output filter) — debug ephemera, pertenecen a commit history. - Mantenido el único gotcha persistente: PyYAML 1.1 parsea `on:` como Python bool True (patrón reutilizable para tests futuros de workflow YAML). Sin tocar (per A8 ratificado en Fase -1): policy.yaml, hooks/**, .claude/skills/**, agents/**, generator/**, templates/**, .claude/rules/skills-map.md. Tests: sin cambios (GREEN ya verde con 665 passed + 1 skipped). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(f4): release.yml — gating de version-match + idempotencia mirror (PR #28 review) Aplica las 4 findings de Copilot review de PR #28. Las 4 son real correctness/idempotency bugs, no estilo. Triage value/effort: todas high/trivial-low → FIX. Gating de version-match (findings 1 + 4): - selftest: needs [version-match]. - build-bundle: needs [version-match]. Antes corrían en paralelo con version-match → CI gastaba tiempo en tags mismatched y contradecía el "orden estricto" documentado. Ahora: version-match → (selftest + build-bundle) → publish-release → mirror-marketplace. Idempotencia mirror-marketplace (findings 2 + 3): - Tras `git add marketplace.json`, si `git diff --cached --quiet` no hay cambios → exit 0. Antes `git commit` no-op fallaba la re-run del workflow. - Antes de `gh pr create`, `gh pr list --head $branch --state open`. Si ya existe un PR abierto → skip create con mensaje. Antes `gh pr create` con PR existente fallaba la re-run. Tests: bin/tests 31/31 verde; full explicit run 850 passed + 1 skipped (skip D5 intencional F3). Sin regresión. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Javier <javier.abril@glassnode.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 2595bf9 commit d5afa8b

12 files changed

Lines changed: 796 additions & 22 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "pos-marketplace",
3+
"owner": {
4+
"name": "javiAI"
5+
},
6+
"metadata": {
7+
"description": "Marketplace for the pos Claude Code plugin.",
8+
"version": "0.1.0"
9+
},
10+
"plugins": [
11+
{
12+
"name": "pos",
13+
"source": {
14+
"source": "github",
15+
"repo": "javiAI/project-operating-system",
16+
"ref": "v0.1.0"
17+
},
18+
"description": "Generador y sistema operativo determinista para proyectos Claude Code: cuestionario → profile → repo generado con hooks, skills, policy, tests, CI/CD.",
19+
"version": "0.1.0"
20+
}
21+
]
22+
}

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://claude.com/schemas/plugin.json",
33
"name": "pos",
44
"displayName": "project-operating-system",
5-
"version": "0.0.1",
5+
"version": "0.1.0",
66
"description": "Generador y sistema operativo determinista para proyectos Claude Code: cuestionario → profile → repo generado con hooks, skills, policy, tests, CI/CD.",
77
"author": {
88
"name": "javiAI",

.claude/rules/ci-cd.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ El hook `pre-push.sh` corre la suite local; GitHub Actions corre la misma suite
3030
- Valida `policy.yaml` vs `.claude/logs/` (skills que deberían haber corrido).
3131
- Escanea dependencias con advisory database (`npm audit`, `pip-audit`).
3232

33-
3. **`.github/workflows/release.yml`** — en tag `v*`:
33+
3. **`.github/workflows/release.yml`** — en tag `v*` (entregado en F4):
3434
- Valida versión en `plugin.json` = tag.
35-
- Publica release en GitHub con assets (plugin bundle).
36-
- Actualiza `javiAI/pos-marketplace` vía PR automático (cuando exista).
35+
- Publica release en GitHub con bundle curated plugin-only.
36+
- Actualiza `javiAI/pos-marketplace` vía PR automático cuando `vars.POS_MARKETPLACE_REPO` está configurado (skippea silenciosamente si no — no bloquea release).
3737

3838
### Job `selftest` (entregado en F3)
3939

@@ -55,6 +55,27 @@ Job dedicado a integración end-to-end del propio plugin `pos`. Corre en `ubuntu
5555

5656
**Drift sintético ↔ meta-repo**: el `policy.yaml` que emite la generación cli-tool todavía tiene el shape pre-D5b (template no migrado). Cada escenario reescribe la sección que necesita (`pre_write` / `pre_pr` / `post_merge` / `skills_allowed`) directamente en `synthetic/policy.yaml`. Esto desacopla la cobertura de D5b de la migración del template (rama propia post-F3).
5757

58+
### Job `release` (entregado en F4)
59+
60+
Workflow dedicado [.github/workflows/release.yml](../../.github/workflows/release.yml). Trigger `push.tags: ['v*']`. Permissions `contents: write` para `gh release create`. Cinco jobs:
61+
62+
- **`version-match`** — primer gate. Asserta `plugin.json.version == ${tag#v}`. Sin esto, el resto no corre.
63+
- **`selftest`** — reusa el contrato F3 (`pytest bin/tests -q`) sobre el repo en el ref del tag. Cubre los 19 nuevos contract tests de F4 (marketplace + release.yml shape + plugin version pin) más los 9 de F3.
64+
- **`build-bundle`** — empaqueta `pos-v${version}.tar.gz` con scope plugin-only curated (`.claude-plugin/`, `.claude/skills/`, `.claude/rules/`, `hooks/`, `agents/`, `policy.yaml`, `bin/pos-selftest.sh`, `bin/_selftest.py`, `docs/RELEASE.md`). Sube como artifact con retention 30d.
65+
- **`publish-release`**`needs: [version-match, selftest, build-bundle]`. Descarga el artifact, llama `gh release create v${version} --generate-notes <bundle>`. **No** crea CHANGELOG.md enforced; usa autogenerated notes (decisión F4 — reabrir si patrón sale corto).
66+
- **`mirror-marketplace`**`if: vars.POS_MARKETPLACE_REPO != ''`. Clona el repo público, sincroniza `marketplace.json`, abre PR vía `gh`. Skippea silenciosamente cuando la variable está vacía (caso por defecto hoy, ya que `javiAI/pos-marketplace` no existe). Activación per-runbook [docs/RELEASE.md § Mirror al marketplace público](../../docs/RELEASE.md).
67+
68+
**Bundle curated, no repo entero**: el consumer del marketplace instala el plugin para usarlo. `generator/`, `tools/`, `templates/`, `questionnaire/`, `bin/tests/`, `.github/`, fixtures quedan fuera (ortogonales al runtime del plugin).
69+
70+
**Source of truth de versión**: `plugin.json.version`. Tag espeja (`v${version}`). `marketplace.json.plugins[0].source.ref` mirror-ea. Los tests `test_marketplace_json_schema.py` + `test_plugin_json_version_bump.py` cruzan los tres.
71+
72+
**Out of scope F4 (diferidos)**:
73+
74+
- `audit.yml` nightly (declarado en `policy.yaml.ci_cd.workflows` desde Fase A; sin consumer activo todavía).
75+
- Skills `/pos:pr-description` y `/pos:release` (sin repetición demostrada — regla #7 CLAUDE.md).
76+
- `CHANGELOG.md` enforced (autogenerated suffices hasta que falle).
77+
- Creación efectiva del repo `javiAI/pos-marketplace` (manual cuando se decida ir live).
78+
5879
## Workflows generados (proyecto destino)
5980

6081
El generador emite workflows según `project_profile.yaml.git_host`. Soportados:

.github/workflows/release.yml

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
name: release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
concurrency:
9+
group: ${{ github.workflow }}-${{ github.ref }}
10+
cancel-in-progress: false
11+
12+
permissions:
13+
contents: write
14+
15+
jobs:
16+
version-match:
17+
name: version-match (plugin.json == tag)
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
22+
23+
- name: Assert plugin.json.version == tag without v
24+
run: |
25+
set -euo pipefail
26+
tag="${GITHUB_REF_NAME}"
27+
expected="${tag#v}"
28+
actual=$(python3 -c "import json; print(json.load(open('.claude-plugin/plugin.json'))['version'])")
29+
if [ "$expected" != "$actual" ]; then
30+
echo "::error ::tag $tag (without v: $expected) != plugin.json.version $actual"
31+
exit 1
32+
fi
33+
echo "ok: tag=$tag matches plugin.json.version=$actual"
34+
35+
selftest:
36+
name: selftest (ubuntu, py 3.11)
37+
runs-on: ubuntu-latest
38+
needs: [version-match]
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
42+
43+
- name: Setup Node
44+
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
45+
with:
46+
node-version-file: .nvmrc
47+
cache: npm
48+
49+
- name: Install Node deps
50+
run: npm ci
51+
52+
- name: Setup Python
53+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
54+
with:
55+
python-version: "3.11"
56+
cache: pip
57+
cache-dependency-path: requirements-dev.txt
58+
59+
- name: Install dev deps
60+
run: python -m pip install --upgrade pip && pip install -r requirements-dev.txt
61+
62+
- name: Pytest selftest (smoke + scenarios)
63+
run: pytest bin/tests -q
64+
65+
build-bundle:
66+
name: build-bundle (curated plugin-only)
67+
runs-on: ubuntu-latest
68+
needs: [version-match]
69+
outputs:
70+
bundle_path: ${{ steps.pack.outputs.bundle_path }}
71+
steps:
72+
- name: Checkout
73+
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
74+
75+
- name: Pack curated bundle
76+
id: pack
77+
run: |
78+
set -euo pipefail
79+
tag="${GITHUB_REF_NAME}"
80+
bundle="pos-${tag}.tar.gz"
81+
tar -czf "$bundle" \
82+
.claude-plugin \
83+
.claude/skills \
84+
.claude/rules \
85+
hooks \
86+
agents \
87+
policy.yaml \
88+
bin/pos-selftest.sh \
89+
bin/_selftest.py \
90+
docs/RELEASE.md
91+
echo "bundle_path=$bundle" >> "$GITHUB_OUTPUT"
92+
echo "ok: built $bundle"
93+
ls -lh "$bundle"
94+
95+
- name: Upload bundle artifact
96+
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45581b # v4.6.0
97+
with:
98+
name: pos-bundle
99+
path: ${{ steps.pack.outputs.bundle_path }}
100+
if-no-files-found: error
101+
retention-days: 30
102+
103+
publish-release:
104+
name: publish-release
105+
runs-on: ubuntu-latest
106+
needs: [version-match, selftest, build-bundle]
107+
steps:
108+
- name: Checkout
109+
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
110+
111+
- name: Download bundle artifact
112+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
113+
with:
114+
name: pos-bundle
115+
116+
- name: Create GitHub release
117+
env:
118+
GH_TOKEN: ${{ github.token }}
119+
run: |
120+
set -euo pipefail
121+
tag="${GITHUB_REF_NAME}"
122+
bundle="pos-${tag}.tar.gz"
123+
gh release create "$tag" \
124+
--title "$tag" \
125+
--generate-notes \
126+
"$bundle"
127+
128+
mirror-marketplace:
129+
name: mirror-marketplace (skippable)
130+
runs-on: ubuntu-latest
131+
needs: [publish-release]
132+
if: ${{ vars.POS_MARKETPLACE_REPO != '' }}
133+
steps:
134+
- name: Checkout
135+
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
136+
137+
- name: Open PR against marketplace repo
138+
env:
139+
GH_TOKEN: ${{ secrets.POS_MARKETPLACE_TOKEN }}
140+
MARKETPLACE_REPO: ${{ vars.POS_MARKETPLACE_REPO }}
141+
run: |
142+
set -euo pipefail
143+
tag="${GITHUB_REF_NAME}"
144+
version="${tag#v}"
145+
if [ -z "${MARKETPLACE_REPO:-}" ]; then
146+
echo "POS_MARKETPLACE_REPO unset; skip mirror"
147+
exit 0
148+
fi
149+
workdir=$(mktemp -d)
150+
cd "$workdir"
151+
gh repo clone "$MARKETPLACE_REPO" mp -- --depth=1
152+
cd mp
153+
branch="bump/pos-${version}"
154+
git checkout -b "$branch"
155+
python3 - <<PY
156+
import json, pathlib
157+
src = json.loads(pathlib.Path('${{ github.workspace }}/.claude-plugin/marketplace.json').read_text())
158+
dst_path = pathlib.Path('marketplace.json')
159+
dst = json.loads(dst_path.read_text()) if dst_path.exists() else src
160+
plugins = dst.get('plugins') or []
161+
target = next((p for p in plugins if p.get('name') == 'pos'), None)
162+
if target is None:
163+
plugins.append(src['plugins'][0])
164+
else:
165+
target['source'] = src['plugins'][0]['source']
166+
target['version'] = src['plugins'][0]['version']
167+
target['description'] = src['plugins'][0].get('description', target.get('description', ''))
168+
dst['plugins'] = plugins
169+
dst_path.write_text(json.dumps(dst, indent=2) + '\n')
170+
PY
171+
git add marketplace.json
172+
if git diff --cached --quiet --exit-code; then
173+
echo "No marketplace changes detected; skip mirror"
174+
exit 0
175+
fi
176+
git -c user.email=actions@github.com -c user.name="github-actions[bot]" \
177+
commit -m "bump pos to ${tag}"
178+
git push -u origin "$branch"
179+
existing_pr=$(gh pr list \
180+
--repo "$MARKETPLACE_REPO" \
181+
--head "$branch" \
182+
--state open \
183+
--json number \
184+
--jq '.[0].number')
185+
if [ -n "$existing_pr" ]; then
186+
echo "Open PR #$existing_pr already exists for branch $branch; skip create"
187+
else
188+
gh pr create \
189+
--repo "$MARKETPLACE_REPO" \
190+
--title "bump pos to ${tag}" \
191+
--body "Automated bump of pos plugin to ${tag}." \
192+
--head "$branch"
193+
fi

0 commit comments

Comments
 (0)