Publish #9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish | |
| # When this runs: | |
| # * On a GitHub Release being published — the common path. Tag the commit | |
| # you want to ship (e.g. `v0.2.0`), draft a release, hit "Publish release" | |
| # in the UI, and this workflow ships it. | |
| # * Manually via "Run workflow" — supports `dry_run: true` so you can | |
| # validate the full pipeline (verify + build + pack + verify tarball) | |
| # without actually pushing to npm. | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: "Skip the actual npm publish (everything else still runs)" | |
| type: boolean | |
| default: false | |
| # id-token: write is REQUIRED for npm provenance — it's how npm verifies the | |
| # tarball was produced by this GitHub Actions workflow on this commit. See | |
| # https://docs.npmjs.com/generating-provenance-statements | |
| permissions: | |
| contents: read | |
| id-token: write | |
| # Never run two publishes in parallel; allow them to queue rather than cancel. | |
| concurrency: | |
| group: publish | |
| cancel-in-progress: false | |
| jobs: | |
| publish: | |
| name: Build, verify, publish | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| # `release` events check out the tagged SHA, which is exactly what we | |
| # want — the tarball provenance attests the commit you tagged. | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v6 | |
| with: | |
| version: 11.3.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'pnpm' | |
| registry-url: 'https://registry.npmjs.org' | |
| scope: '@vymalo' | |
| always-auth: true | |
| - name: Install (frozen lockfile) | |
| # Guarantees the installed tree exactly matches pnpm-lock.yaml — no | |
| # silent dependency drift between dev and the publish run. | |
| run: pnpm install --frozen-lockfile | |
| - name: Audit dependencies (fail on high+ severity) | |
| # Hard-fail on high/critical CVEs. Moderate/low are tracked but don't | |
| # gate the release — moderate-severity Node CVEs are noisy. | |
| run: pnpm audit --audit-level=high --prod | |
| - name: Typecheck | |
| run: pnpm -r typecheck | |
| - name: Test | |
| run: pnpm -r test | |
| - name: Lint | |
| run: pnpm lint | |
| - name: Format check | |
| run: pnpm format:check | |
| - name: Build oauth2 plugin (tsc → packages/opencode-oauth2/dist) | |
| run: pnpm --filter @vymalo/opencode-oauth2 build | |
| - name: Build oauth2 release bundle (Rolldown) | |
| # Not what we publish to npm (that's the tsc output), but we still | |
| # build it on every publish so a broken bundler is caught here. | |
| run: pnpm --filter @vymalo/opencode-oauth2-bundle build | |
| - name: Build models-info plugin (tsc → packages/opencode-models-info/dist) | |
| run: pnpm --filter @vymalo/opencode-models-info build | |
| - name: Pack oauth2 tarball (for inspection and as a release asset) | |
| run: | | |
| mkdir -p dist-tarball/oauth2 | |
| pnpm --filter @vymalo/opencode-oauth2 pack \ | |
| --pack-destination "$PWD/dist-tarball/oauth2" | |
| ls -la dist-tarball/oauth2/ | |
| - name: Pack models-info tarball | |
| run: | | |
| mkdir -p dist-tarball/models-info | |
| pnpm --filter @vymalo/opencode-models-info pack \ | |
| --pack-destination "$PWD/dist-tarball/models-info" | |
| ls -la dist-tarball/models-info/ | |
| - name: Verify tarball contents | |
| # Defense-in-depth: confirms each package is shipping its dist/, NOT | |
| # shipping source or tests, and that the package isn't accidentally | |
| # empty. Runs against both tarballs. | |
| run: | | |
| set -euo pipefail | |
| verify_tarball() { | |
| local tarball="$1" | |
| echo "=== Tarball: $tarball ===" | |
| # List once into a variable. We then match against the variable | |
| # with bash pattern matching — no pipes, no SIGPIPE, immune to | |
| # the `grep -q | pipefail` trap regardless of listing size. | |
| local entries | |
| entries=$(tar -tzf "$tarball") | |
| printf '%s\n' "$entries" | |
| echo | |
| echo "=== Validation: $tarball ===" | |
| # Wrap with newlines so a literal full-line match becomes a | |
| # simple substring search. | |
| local haystack=$'\n'"$entries"$'\n' | |
| if [[ "$haystack" != *$'\n'"package/dist/index.js"$'\n'* ]]; then | |
| echo "FAIL: package/dist/index.js missing from $tarball" | |
| exit 1 | |
| fi | |
| if [[ "$haystack" =~ $'\n'package/(test|src|node_modules)/ ]]; then | |
| echo "FAIL: $tarball contains forbidden directories (test/src/node_modules)" | |
| echo "Offending entries:" | |
| printf '%s\n' "$entries" | grep -E '^package/(test|src|node_modules)/' || true | |
| exit 1 | |
| fi | |
| if [[ "$haystack" != *$'\n'"package/LICENSE"$'\n'* ]]; then | |
| echo "FAIL: LICENSE missing from $tarball" | |
| exit 1 | |
| fi | |
| echo "OK: $tarball" | |
| } | |
| for tarball in dist-tarball/oauth2/*.tgz dist-tarball/models-info/*.tgz; do | |
| verify_tarball "$tarball" | |
| done | |
| - name: Publish oauth2 to npm (with provenance) | |
| if: ${{ inputs.dry_run != true }} | |
| # --provenance: generates a signed SLSA L2 attestation linking the | |
| # tarball to this workflow run + commit SHA. npm | |
| # verifies it server-side; consumers can verify | |
| # locally via `npm install --provenance` or by | |
| # inspecting the package page on npmjs.com. | |
| # --no-git-checks: bypass pnpm's "are you on a clean main branch?" | |
| # guard. Release events check out a detached HEAD on | |
| # the tag SHA, which would otherwise trip the check. | |
| # --access public: required for first publish under a new scope. | |
| # Skips publish if the version is already on the registry — so a | |
| # release that only bumps one package's version doesn't fail. | |
| run: | | |
| set -e | |
| version=$(node -p "require('./packages/opencode-oauth2/package.json').version") | |
| if npm view "@vymalo/opencode-oauth2@${version}" version >/dev/null 2>&1; then | |
| echo "::notice::@vymalo/opencode-oauth2@${version} already on npm — skipping." | |
| exit 0 | |
| fi | |
| pnpm --filter @vymalo/opencode-oauth2 publish \ | |
| --access public \ | |
| --no-git-checks \ | |
| --provenance | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.MCP_NPM_AUTH_TOKEN }} | |
| # Belt and braces — also enables provenance for any underlying npm | |
| # CLI invocation that pnpm delegates to. | |
| NPM_CONFIG_PROVENANCE: "true" | |
| - name: Publish models-info to npm (with provenance) | |
| if: ${{ inputs.dry_run != true }} | |
| run: | | |
| set -e | |
| version=$(node -p "require('./packages/opencode-models-info/package.json').version") | |
| if npm view "@vymalo/opencode-models-info@${version}" version >/dev/null 2>&1; then | |
| echo "::notice::@vymalo/opencode-models-info@${version} already on npm — skipping." | |
| exit 0 | |
| fi | |
| pnpm --filter @vymalo/opencode-models-info publish \ | |
| --access public \ | |
| --no-git-checks \ | |
| --provenance | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.MCP_NPM_AUTH_TOKEN }} | |
| NPM_CONFIG_PROVENANCE: "true" | |
| - name: Dry-run notice | |
| if: ${{ inputs.dry_run == true }} | |
| run: | | |
| echo "::notice::DRY RUN — skipped npm publish." | |
| echo "Tarball ready at: dist-tarball/" | |
| - name: Attach tarball to GitHub Release | |
| if: ${{ github.event_name == 'release' && inputs.dry_run != true }} | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| # The tarball alongside the release page makes downstream | |
| # verification trivial (no need to fetch from npm just to inspect). | |
| files: dist-tarball/**/*.tgz |