diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..443fe0a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: Release + +# Two independent jobs, both triggered by publishing a GitHub Release: +# 1. pack — builds the tarball, renames it to the version-free asset name and +# attaches it to the release (the market's `latest/download` link +# depends on that exact name staying free of a version number). +# 2. publish — publishes to npm. Enabled only after this repository is +# configured as a Trusted Publisher on npmjs.com and the repo +# variable NPM_TRUSTED_PUBLISHING is set to "true"; until then the +# job is skipped, so a release never fails on missing credentials. +# +# Setup for job 2 (one time, by the package owner): +# npmjs.com → the package → Settings → Trusted Publisher → GitHub Actions: +# Organization/user: czcdr Repository: dsh-plugin-rea +# Workflow filename: release.yml Environment: (leave empty) +# then: repo Settings → Secrets and variables → Actions → Variables → +# NPM_TRUSTED_PUBLISHING = true + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: write + +jobs: + pack: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Verify the released tag matches package.json + run: | + tag="${{ github.event.release.tag_name }}" + ver="v$(node -p "require('./package.json').version")" + if [ -n "$tag" ] && [ "$tag" != "$ver" ]; then + echo "FAIL: release tag $tag does not match package.json $ver" + exit 1 + fi + echo "tag matches: ${tag:-}" + + - name: Pack and attach a version-free tarball + env: + GH_TOKEN: ${{ github.token }} + run: | + npm pack + mv dsh-plugin-rea-*.tgz dsh-plugin-rea.tgz + ls -l dsh-plugin-rea.tgz + if [ -n "${{ github.event.release.tag_name }}" ]; then + gh release upload "${{ github.event.release.tag_name }}" dsh-plugin-rea.tgz --clobber + fi + + publish: + needs: pack + if: ${{ vars.NPM_TRUSTED_PUBLISHING == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: npm CLI must support OIDC trusted publishing (>= 11.15.0) + run: npm install -g npm@latest && npm --version + + - name: Publish to npm via trusted publishing + run: npm publish --provenance --access public diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..6d98950 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,104 @@ +name: Validate + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Bundle manifest sanity + run: | + node -e " + const p = require('./package.json'); + const fail = (m) => { console.error('FAIL: ' + m); process.exit(1) }; + if (!p.dsh || !p.dsh.bundle || !p.dsh.bundle.patch) fail('package.json is missing dsh.bundle.patch'); + if (p.private) fail('package.json must not be private to be publishable'); + for (const f of ['cordis.patch.yml', 'README.md', 'LICENSE', 'check-updates.ps1']) { + if (!p.files.includes(f)) fail('files[] must ship ' + f); + } + if (!require('fs').existsSync(p.dsh.bundle.patch)) fail('dsh.bundle.patch target does not exist'); + console.log('manifest ok'); + " + + - name: JSON files parse + run: | + node -e " + for (const f of ['package.json', 'screenshots.json']) { + JSON.parse(require('fs').readFileSync(f, 'utf8')); + console.log('json ok:', f); + } + " + + - name: The npm payload ships every market-facing file + run: | + # Regression guard for files[]: the market reads screenshots.json and the + # images next to package.json, and a user copies examples/ by hand — none of + # that is worth anything if npm pack drops it. + node -e " + const { execSync } = require('child_process'); + const meta = JSON.parse(execSync('npm pack --dry-run --json', { encoding: 'utf8' }))[0]; + const files = meta.files.map((f) => f.path); + const need = [ + 'cordis.patch.yml', 'README.md', 'LICENSE', 'CHANGELOG.md', 'check-updates.ps1', + 'screenshots.json', 'assets/screenshot-1.png', 'assets/screenshot-2.png', + 'examples/cordis.patch.posix.yml', 'examples/cordis.patch.with-env.yml', + ]; + const missing = need.filter((f) => !files.includes(f)); + if (missing.length) { + console.error('FAIL: the npm payload is missing ' + missing.join(', ')); + process.exit(1); + } + console.log('npm payload ok: ' + files.length + ' files, all market-facing files included'); + " + + - name: YAML files parse + run: | + python3 -m pip install --quiet --disable-pip-version-check pyyaml + python3 - <<'PY' + import glob, sys, yaml + files = ['cordis.patch.yml', 'docs/marketplace-entry.yml'] + sorted(glob.glob('examples/*.yml')) + sorted(glob.glob('.github/workflows/*.yml')) + for f in files: + with open(f, encoding='utf-8') as fh: + yaml.safe_load(fh) + print('yaml ok:', f) + PY + + - name: The shipped patch stays machine-independent + run: | + set -e + if grep -nE '[A-Za-z]:\\\\|[A-Za-z]:\\' cordis.patch.yml; then + echo "FAIL: cordis.patch.yml must not contain an absolute Windows path" + exit 1 + fi + if grep -nE '/(home|Users)/[A-Za-z0-9._-]+' cordis.patch.yml; then + echo "FAIL: cordis.patch.yml must not contain an absolute POSIX home path" + exit 1 + fi + echo "patch ok: no machine-specific path" + + - name: The row keeps its contract + run: | + python3 - <<'PY' + import sys, yaml + doc = yaml.safe_load(open('cordis.patch.yml', encoding='utf-8')) + row = doc[0]['insert'][0] + assert row['id'] == 'mcp-rea', row + assert row['name'] == '@deepseek-ai/dsh-mcp-client', row + cfg = row['config'] + assert cfg['serverName'] == 'rea', cfg + assert cfg['transport'] == 'stdio', cfg + assert 'command' in cfg and 'args' in cfg, cfg + print('row ok:', cfg['command'], cfg['args']) + PY