Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "pos-marketplace",
"owner": {
"name": "javiAI"
},
"metadata": {
"description": "Marketplace for the pos Claude Code plugin.",
"version": "0.1.0"
},
"plugins": [
{
"name": "pos",
"source": {
"source": "github",
"repo": "javiAI/project-operating-system",
"ref": "v0.1.0"
},
"description": "Generador y sistema operativo determinista para proyectos Claude Code: cuestionario → profile → repo generado con hooks, skills, policy, tests, CI/CD.",
"version": "0.1.0"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://claude.com/schemas/plugin.json",
"name": "pos",
"displayName": "project-operating-system",
"version": "0.0.1",
"version": "0.1.0",
"description": "Generador y sistema operativo determinista para proyectos Claude Code: cuestionario → profile → repo generado con hooks, skills, policy, tests, CI/CD.",
"author": {
"name": "javiAI",
Expand Down
27 changes: 24 additions & 3 deletions .claude/rules/ci-cd.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ El hook `pre-push.sh` corre la suite local; GitHub Actions corre la misma suite
- Valida `policy.yaml` vs `.claude/logs/` (skills que deberían haber corrido).
- Escanea dependencias con advisory database (`npm audit`, `pip-audit`).

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

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

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

**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).

### Job `release` (entregado en F4)

Workflow dedicado [.github/workflows/release.yml](../../.github/workflows/release.yml). Trigger `push.tags: ['v*']`. Permissions `contents: write` para `gh release create`. Cinco jobs:

- **`version-match`** — primer gate. Asserta `plugin.json.version == ${tag#v}`. Sin esto, el resto no corre.
- **`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.
- **`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.
- **`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).
- **`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).

**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).

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

**Out of scope F4 (diferidos)**:

- `audit.yml` nightly (declarado en `policy.yaml.ci_cd.workflows` desde Fase A; sin consumer activo todavía).
- Skills `/pos:pr-description` y `/pos:release` (sin repetición demostrada — regla #7 CLAUDE.md).
- `CHANGELOG.md` enforced (autogenerated suffices hasta que falle).
- Creación efectiva del repo `javiAI/pos-marketplace` (manual cuando se decida ir live).

## Workflows generados (proyecto destino)

El generador emite workflows según `project_profile.yaml.git_host`. Soportados:
Expand Down
193 changes: 193 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
name: release

on:
push:
tags:
- "v*"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: write

jobs:
version-match:
name: version-match (plugin.json == tag)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7

- name: Assert plugin.json.version == tag without v
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
expected="${tag#v}"
actual=$(python3 -c "import json; print(json.load(open('.claude-plugin/plugin.json'))['version'])")
if [ "$expected" != "$actual" ]; then
echo "::error ::tag $tag (without v: $expected) != plugin.json.version $actual"
exit 1
fi
echo "ok: tag=$tag matches plugin.json.version=$actual"

selftest:
name: selftest (ubuntu, py 3.11)
runs-on: ubuntu-latest
needs: [version-match]
steps:
Comment on lines +35 to +39
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selftest is described as being gated by version-match ("primer gate; sin esto el resto no corre"), but the workflow currently runs selftest in parallel with version-match (no needs). This can waste CI time on mismatched tags and contradicts the documented job ordering; add needs: [version-match] to selftest to make version-match a true gate.

Copilot uses AI. Check for mistakes.
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7

- name: Setup Node
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version-file: .nvmrc
cache: npm

- name: Install Node deps
run: npm ci

- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.11"
cache: pip
cache-dependency-path: requirements-dev.txt

- name: Install dev deps
run: python -m pip install --upgrade pip && pip install -r requirements-dev.txt

- name: Pytest selftest (smoke + scenarios)
run: pytest bin/tests -q

build-bundle:
name: build-bundle (curated plugin-only)
runs-on: ubuntu-latest
needs: [version-match]
outputs:
Comment on lines +65 to +69
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build-bundle is also meant to be behind the version-match gate, but it currently has no needs and will run even when the tag/version mismatch. Add needs: [version-match] (or otherwise enforce the ordering) so the bundle build is skipped when version-match fails and the job graph matches the documented "orden estricto".

Copilot uses AI. Check for mistakes.
bundle_path: ${{ steps.pack.outputs.bundle_path }}
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7

- name: Pack curated bundle
id: pack
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
bundle="pos-${tag}.tar.gz"
tar -czf "$bundle" \
.claude-plugin \
.claude/skills \
.claude/rules \
hooks \
agents \
policy.yaml \
bin/pos-selftest.sh \
bin/_selftest.py \
docs/RELEASE.md
echo "bundle_path=$bundle" >> "$GITHUB_OUTPUT"
echo "ok: built $bundle"
ls -lh "$bundle"

- name: Upload bundle artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45581b # v4.6.0
with:
name: pos-bundle
path: ${{ steps.pack.outputs.bundle_path }}
if-no-files-found: error
retention-days: 30

publish-release:
name: publish-release
runs-on: ubuntu-latest
needs: [version-match, selftest, build-bundle]
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7

- name: Download bundle artifact
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: pos-bundle

- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
bundle="pos-${tag}.tar.gz"
gh release create "$tag" \
--title "$tag" \
--generate-notes \
"$bundle"

mirror-marketplace:
name: mirror-marketplace (skippable)
runs-on: ubuntu-latest
needs: [publish-release]
if: ${{ vars.POS_MARKETPLACE_REPO != '' }}
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7

- name: Open PR against marketplace repo
env:
GH_TOKEN: ${{ secrets.POS_MARKETPLACE_TOKEN }}
MARKETPLACE_REPO: ${{ vars.POS_MARKETPLACE_REPO }}
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
version="${tag#v}"
if [ -z "${MARKETPLACE_REPO:-}" ]; then
echo "POS_MARKETPLACE_REPO unset; skip mirror"
exit 0
fi
workdir=$(mktemp -d)
cd "$workdir"
gh repo clone "$MARKETPLACE_REPO" mp -- --depth=1
cd mp
branch="bump/pos-${version}"
git checkout -b "$branch"
python3 - <<PY
import json, pathlib
src = json.loads(pathlib.Path('${{ github.workspace }}/.claude-plugin/marketplace.json').read_text())
dst_path = pathlib.Path('marketplace.json')
dst = json.loads(dst_path.read_text()) if dst_path.exists() else src
plugins = dst.get('plugins') or []
target = next((p for p in plugins if p.get('name') == 'pos'), None)
if target is None:
plugins.append(src['plugins'][0])
else:
target['source'] = src['plugins'][0]['source']
target['version'] = src['plugins'][0]['version']
target['description'] = src['plugins'][0].get('description', target.get('description', ''))
dst['plugins'] = plugins
dst_path.write_text(json.dumps(dst, indent=2) + '\n')
PY
git add marketplace.json
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mirror-marketplace will fail on workflow re-runs (or any no-op update) because git commit -m ... exits non-zero when there are no changes. Make the job idempotent by checking for a clean working tree before committing (and exiting 0 if nothing changed) or by handling the empty-commit case explicitly.

Suggested change
git add marketplace.json
git add marketplace.json
if git diff --cached --quiet --exit-code; then
echo "No marketplace changes detected; skip mirror"
exit 0
fi

Copilot uses AI. Check for mistakes.
if git diff --cached --quiet --exit-code; then
echo "No marketplace changes detected; skip mirror"
exit 0
fi
git -c user.email=actions@github.com -c user.name="github-actions[bot]" \
commit -m "bump pos to ${tag}"
git push -u origin "$branch"
existing_pr=$(gh pr list \
--repo "$MARKETPLACE_REPO" \
--head "$branch" \
--state open \
--json number \
--jq '.[0].number')
if [ -n "$existing_pr" ]; then
echo "Open PR #$existing_pr already exists for branch $branch; skip create"
else
gh pr create \
--repo "$MARKETPLACE_REPO" \
--title "bump pos to ${tag}" \
--body "Automated bump of pos plugin to ${tag}." \
--head "$branch"
fi
Loading
Loading