From 0e5fb17bdf62a5c7c5b6db9a42e77bf5bddb4407 Mon Sep 17 00:00:00 2001 From: Stephane Segning Lambou Date: Mon, 25 May 2026 03:04:28 +0200 Subject: [PATCH 1/2] feat(ci): add reusable composite action for plugin install Add `.github/actions/setup` so downstream CI workflows can install @vymalo/opencode-oauth2 (and optionally the opencode CLI) in one step, with cross-run caching of the global node_modules. This avoids paying for a full `pnpm install` in consumer pipelines that just want to invoke opencode with the plugin loaded. Usage: uses: vymalo/opencode-oauth2/.github/actions/setup@v0.2.0 with: node-version: '22' install-opencode: 'true' Update the package README's GitHub Actions section to point at the composite action instead of an ad-hoc `npm install -g` step. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/actions/setup/README.md | 82 +++++++++++++++++++ .github/actions/setup/action.yml | 127 +++++++++++++++++++++++++++++ .github/workflows/opencode-run.yml | 32 ++++---- docs/github-actions.md | 42 ++++++++-- 4 files changed, 262 insertions(+), 21 deletions(-) create mode 100644 .github/actions/setup/README.md create mode 100644 .github/actions/setup/action.yml diff --git a/.github/actions/setup/README.md b/.github/actions/setup/README.md new file mode 100644 index 0000000..c4bd5ff --- /dev/null +++ b/.github/actions/setup/README.md @@ -0,0 +1,82 @@ +# `vymalo/opencode-oauth2/.github/actions/setup` + +Composite GitHub Action that installs [`@vymalo/opencode-oauth2`](https://www.npmjs.com/package/@vymalo/opencode-oauth2) (and optionally the `opencode` CLI) into the runner's global `node_modules`, with cross-run caching so the install runs once per version. + +## Why + +When you use OpenCode in CI — e.g. an AI-assisted job that runs `opencode run --model ...` — you don't want to pay for a full `pnpm install` of your repo just to make the plugin available. This action does the minimum: one cached `npm install -g` of the plugin (and CLI, if you ask for it), then exports `NODE_PATH` so OpenCode finds the plugin no matter where you `cd` to. + +On a cache hit the install step is skipped entirely. + +## Usage + +### Minimal — plugin only (you already have `opencode` installed) + +```yaml +- uses: vymalo/opencode-oauth2/.github/actions/setup@v0.2.0 + with: + node-version: '22' +``` + +### Plugin + opencode CLI + +```yaml +- uses: vymalo/opencode-oauth2/.github/actions/setup@v0.2.0 + with: + node-version: '22' + install-opencode: 'true' +``` + +### Full federated-identity job (no long-lived secrets) + +```yaml +name: AI-assisted job + +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: vymalo/opencode-oauth2/.github/actions/setup@v0.2.0 + with: + node-version: '22' + install-opencode: 'true' + + - run: opencode run --model "example-ai/glm-5" "summarize the diff" + env: + OPENCODE_CONFIG_DIR: ${{ github.workspace }}/.opencode-ci +``` + +The `OPENCODE_CONFIG_DIR` should contain an `opencode.json` like the one in the [federated identity section](../../../packages/opencode-oauth2/README.md#federated-identity-no-long-lived-secrets-in-ci) of the package README. + +## Inputs + +| Name | Default | Description | +| --- | --- | --- | +| `version` | `latest` | Plugin version (`"0.2.0"`, `"^0.2.0"`, `"next"`, …). | +| `install-opencode` | `false` | Also install the opencode CLI globally. | +| `opencode-package` | `opencode-ai` | npm package name for the CLI. Override for forks/mirrors. | +| `opencode-version` | `latest` | CLI version. Only used when `install-opencode=true`. | +| `node-version` | _(unset)_ | If set, runs `actions/setup-node@v4` with this version first. | +| `cache` | `true` | Cache the global install between runs. | + +## Outputs + +| Name | Description | +| --- | --- | +| `version` | Resolved plugin version that was installed. | +| `opencode-version` | Resolved CLI version (empty if `install-opencode=false`). | +| `node-path` | Absolute path to the global `node_modules`. Also exported as `NODE_PATH`. | +| `cache-hit` | `"true"` when the install was restored from cache. | + +## Pinning + +Pin to a release tag (`@v0.2.0`) — the action ships with the plugin in the same repo, so the tag determines what gets installed by default. If you set `version:` explicitly the action uses that and only the action.yml itself is taken from the tag ref. diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..1b5a3f2 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,127 @@ +name: 'Setup @vymalo/opencode-oauth2' +description: >- + Install the @vymalo/opencode-oauth2 OpenCode plugin (and optionally the + opencode CLI) into the runner's global node_modules, with cross-run caching + so subsequent runs skip the install entirely. +author: 'vymalo contributors' +branding: + icon: 'lock' + color: 'purple' + +inputs: + version: + description: >- + Version of @vymalo/opencode-oauth2 to install. Accepts any valid npm + version spec ("0.2.0", "^0.2.0", "next"). Defaults to "latest". + required: false + default: 'latest' + install-opencode: + description: >- + If "true", also installs the opencode CLI globally so the runner can + invoke `opencode run ...` without a separate step. + required: false + default: 'false' + opencode-package: + description: >- + npm package name for the opencode CLI. Override if you publish a fork + or use a private mirror. + required: false + default: 'opencode-ai' + opencode-version: + description: 'opencode CLI version. Used only when install-opencode=true.' + required: false + default: 'latest' + node-version: + description: >- + If set, the action runs actions/setup-node@v4 with this version first. + Leave empty to assume the caller has already set up Node. + required: false + default: '' + cache: + description: 'Cache the global install between runs. Defaults to "true".' + required: false + default: 'true' + +outputs: + version: + description: 'Resolved plugin version that was installed.' + value: ${{ steps.resolve.outputs.plugin-version }} + opencode-version: + description: >- + Resolved opencode CLI version. Empty when install-opencode=false. + value: ${{ steps.resolve.outputs.opencode-version }} + node-path: + description: >- + Absolute path to the global node_modules directory where the plugin was + installed. Already exported as NODE_PATH for subsequent steps. + value: ${{ steps.resolve.outputs.modules }} + cache-hit: + description: '"true" when the install was restored from cache.' + value: ${{ steps.cache.outputs.cache-hit }} + +runs: + using: 'composite' + steps: + - name: Setup Node.js + if: inputs.node-version != '' + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Resolve versions and paths + id: resolve + shell: bash + run: | + set -euo pipefail + + plugin_spec='${{ inputs.version }}' + if [ "$plugin_spec" = "latest" ]; then + plugin_version=$(npm view @vymalo/opencode-oauth2 version) + else + plugin_version="$plugin_spec" + fi + echo "plugin-version=$plugin_version" >> "$GITHUB_OUTPUT" + + opencode_version="" + if [ '${{ inputs.install-opencode }}' = 'true' ]; then + ov_spec='${{ inputs.opencode-version }}' + if [ "$ov_spec" = "latest" ]; then + opencode_version=$(npm view '${{ inputs.opencode-package }}' version) + else + opencode_version="$ov_spec" + fi + fi + echo "opencode-version=$opencode_version" >> "$GITHUB_OUTPUT" + + prefix=$(npm config get prefix) + if [ "$RUNNER_OS" = "Windows" ]; then + modules="$prefix/node_modules" + else + modules="$prefix/lib/node_modules" + fi + echo "prefix=$prefix" >> "$GITHUB_OUTPUT" + echo "modules=$modules" >> "$GITHUB_OUTPUT" + + - name: Restore install cache + if: inputs.cache == 'true' + id: cache + uses: actions/cache@v4 + with: + path: ${{ steps.resolve.outputs.modules }} + key: vymalo-opencode-oauth2-${{ runner.os }}-plugin-${{ steps.resolve.outputs.plugin-version }}-cli-${{ steps.resolve.outputs.opencode-version || 'none' }} + + - name: Install plugin (and optionally opencode) + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + run: | + set -euo pipefail + pkgs=( "@vymalo/opencode-oauth2@${{ steps.resolve.outputs.plugin-version }}" ) + if [ '${{ inputs.install-opencode }}' = 'true' ]; then + pkgs+=( "${{ inputs.opencode-package }}@${{ steps.resolve.outputs.opencode-version }}" ) + fi + npm install -g --no-audit --no-fund "${pkgs[@]}" + + - name: Export NODE_PATH + shell: bash + run: | + echo "NODE_PATH=${{ steps.resolve.outputs.modules }}" >> "$GITHUB_ENV" diff --git a/.github/workflows/opencode-run.yml b/.github/workflows/opencode-run.yml index 01eb525..20beff6 100644 --- a/.github/workflows/opencode-run.yml +++ b/.github/workflows/opencode-run.yml @@ -75,23 +75,25 @@ jobs: - name: Checkout caller repo uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + # The reusable workflow lives in vymalo/opencode-oauth2, so we also need + # to check out THIS repo to a side path to load the composite action from + # the same ref the caller pinned. `github.job_workflow_sha` is the SHA of + # this reusable workflow file's commit — guarantees action + workflow stay + # in lockstep. + - name: Checkout opencode-oauth2 (for the setup action) + uses: actions/checkout@v4 with: - node-version: ${{ inputs.node-version }} + repository: vymalo/opencode-oauth2 + ref: ${{ github.job_workflow_sha }} + path: .opencode-oauth2-action - - name: Install opencode + plugin - run: | - set -euo pipefail - opencode_spec="opencode" - if [ -n "${{ inputs.opencode-version }}" ]; then - opencode_spec="opencode@${{ inputs.opencode-version }}" - fi - plugin_spec="@vymalo/opencode-oauth2" - if [ -n "${{ inputs.plugin-version }}" ]; then - plugin_spec="@vymalo/opencode-oauth2@${{ inputs.plugin-version }}" - fi - npm install -g "$opencode_spec" "$plugin_spec" + - name: Install opencode + plugin (cached) + uses: ./.opencode-oauth2-action/.github/actions/setup + with: + node-version: ${{ inputs.node-version }} + version: ${{ inputs.plugin-version || 'latest' }} + install-opencode: 'true' + opencode-version: ${{ inputs.opencode-version || 'latest' }} - name: Verify opencode config exists run: | diff --git a/docs/github-actions.md b/docs/github-actions.md index 4e24c64..9833317 100644 --- a/docs/github-actions.md +++ b/docs/github-actions.md @@ -87,13 +87,37 @@ jobs: The reusable workflow handles: -- Installing pnpm + Node 22. -- `npm install -g opencode @vymalo/opencode-oauth2`. +- Installing Node 22. +- Installing the plugin + opencode CLI via the [setup composite action](#composite-action-bring-your-own-job), which caches the global `node_modules` between runs. - Pointing `OPENCODE_CONFIG_DIR` at the directory you specify. - Running `opencode run --model "" ""`. You're responsible for committing `.opencode-ci/opencode.json` (or wherever you pointed `opencode-config-path`) with the `authFlow: "jwt_bearer"` config. +## Composite action (bring-your-own-job) + +If you need to compose your own job — different runner image, custom pre/post steps, multiple `opencode run` calls in one job — use the composite setup action directly. It installs the plugin (and optionally the CLI) globally and caches the install across runs. + +```yaml +- uses: vymalo/opencode-oauth2/.github/actions/setup@v0.2.0 + with: + node-version: '22' + install-opencode: 'true' +``` + +| Input | Default | Purpose | +| --- | --- | --- | +| `version` | `latest` | Plugin version (`"0.2.0"`, `"^0.2.0"`, `"next"`, …). | +| `install-opencode` | `false` | Also install the opencode CLI globally. | +| `opencode-package` | `opencode-ai` | npm package name for the CLI. Override for forks/mirrors. | +| `opencode-version` | `latest` | CLI version. Used only when `install-opencode=true`. | +| `node-version` | _(unset)_ | If set, runs `actions/setup-node@v4` first. Leave empty if you already set up Node. | +| `cache` | `true` | Cache the global install between runs. | + +Outputs: `version`, `opencode-version`, `node-path`, `cache-hit`. `NODE_PATH` is exported for subsequent steps so `opencode` finds the plugin no matter what cwd you `cd` to. Full reference in [`.github/actions/setup/README.md`](../.github/actions/setup/README.md). + +On a cache hit (same version, same runner OS) the install step is a no-op — useful for high-frequency triggers like `pull_request`. + ## Minimal worked example `.github/workflows/ai.yml` in your repo: @@ -114,7 +138,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - run: npm install -g opencode @vymalo/opencode-oauth2 + - uses: vymalo/opencode-oauth2/.github/actions/setup@v0.2.0 + with: + node-version: '22' + install-opencode: 'true' - run: opencode run --model "miaou/glm-5" "Summarize the changes in this PR" > summary.md env: OPENCODE_CONFIG_DIR: "${{ github.workspace }}/.opencode-ci" @@ -174,10 +201,10 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: vymalo/opencode-oauth2/.github/actions/setup@v0.2.0 with: node-version: ${{ matrix.node }} - - run: npm install -g opencode @vymalo/opencode-oauth2 + install-opencode: 'true' - run: opencode run --model "miaou/glm-5" "say hi from ${{ matrix.os }} node${{ matrix.node }}" env: OPENCODE_CONFIG_DIR: "${{ github.workspace }}/.opencode-ci" @@ -245,7 +272,10 @@ jobs: # Code from the PR is treated as untrusted input. with: ref: ${{ github.event.pull_request.base.ref }} - - run: npm install -g opencode @vymalo/opencode-oauth2 + - uses: vymalo/opencode-oauth2/.github/actions/setup@v0.2.0 + with: + node-version: '22' + install-opencode: 'true' # Read the PR diff but do not execute fork-provided code. - id: diff run: gh pr diff ${{ github.event.pull_request.number }} > /tmp/diff.patch From a2acd5d90c30d85e6e8a4db86262108437626de3 Mon Sep 17 00:00:00 2001 From: Stephane Segning Lambou Date: Mon, 25 May 2026 03:30:30 +0200 Subject: [PATCH 2/2] fix(ci): address review feedback on composite action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 fixes: - Route all `inputs.*` interpolation through `env:` vars so values never get spliced into shell script bodies (Gemini, both steps). - Cache the entire install prefix (not just `lib/node_modules`) so bin shims like `opencode` are restored on cache hit (Gemini, Codex). - Prepend the prefix bin dir to `GITHUB_PATH` so CLIs are invocable in subsequent steps (Gemini). - Drop the side-checkout trick in the reusable workflow that depended on `github.job_workflow_sha` (not a real context property — would silently fall back to default branch). The reusable workflow now inlines install + cache directly (Codex). P2 fixes: - Resolve mutable specs (`latest`, `next`, `^0.2.0`) to a concrete version via `npm view` BEFORE deriving the cache key, so a moving dist-tag can never pin a stale install (Codex, ×2). - Include resolved Node version in the cache key (ABI + prefix layout vary per Node version) (Codex). - Include `opencode-package` in the cache key so forks/mirrors with the same version string don't collide on the same cache entry (Codex). Also: use a runner-local prefix (`$HOME/.opencode-oauth2-setup`) instead of npm's default global prefix — avoids needing sudo on hosted runners and gives a hermetic cache layout that's identical across OSes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/actions/setup/README.md | 19 ++++-- .github/actions/setup/action.yml | 98 ++++++++++++++++++++++-------- .github/workflows/opencode-run.yml | 75 ++++++++++++++++++----- 3 files changed, 147 insertions(+), 45 deletions(-) diff --git a/.github/actions/setup/README.md b/.github/actions/setup/README.md index c4bd5ff..05b9bd4 100644 --- a/.github/actions/setup/README.md +++ b/.github/actions/setup/README.md @@ -1,12 +1,12 @@ # `vymalo/opencode-oauth2/.github/actions/setup` -Composite GitHub Action that installs [`@vymalo/opencode-oauth2`](https://www.npmjs.com/package/@vymalo/opencode-oauth2) (and optionally the `opencode` CLI) into the runner's global `node_modules`, with cross-run caching so the install runs once per version. +Composite GitHub Action that installs [`@vymalo/opencode-oauth2`](https://www.npmjs.com/package/@vymalo/opencode-oauth2) (and optionally the `opencode` CLI) into a runner-local prefix (`$HOME/.opencode-oauth2-setup`), with cross-run caching so the install runs once per (OS, Node version, package version) tuple. ## Why -When you use OpenCode in CI — e.g. an AI-assisted job that runs `opencode run --model ...` — you don't want to pay for a full `pnpm install` of your repo just to make the plugin available. This action does the minimum: one cached `npm install -g` of the plugin (and CLI, if you ask for it), then exports `NODE_PATH` so OpenCode finds the plugin no matter where you `cd` to. +When you use OpenCode in CI — e.g. an AI-assisted job that runs `opencode run --model ...` — you don't want to pay for a full `pnpm install` of your repo just to make the plugin available. This action does the minimum: one cached `npm install -g --prefix ` of the plugin (and CLI, if you ask for it), then prepends the prefix's `bin/` to `PATH` and exports `NODE_PATH` so OpenCode resolves the plugin no matter where you `cd` to. -On a cache hit the install step is skipped entirely. +On a cache hit both `lib/node_modules` and `bin/` (the CLI shims) are restored from the cache — `opencode` is invocable with no install step. ## Usage @@ -74,9 +74,20 @@ The `OPENCODE_CONFIG_DIR` should contain an `opencode.json` like the one in the | --- | --- | | `version` | Resolved plugin version that was installed. | | `opencode-version` | Resolved CLI version (empty if `install-opencode=false`). | -| `node-path` | Absolute path to the global `node_modules`. Also exported as `NODE_PATH`. | +| `node-path` | Absolute path to the prefix's `node_modules`. Also exported as `NODE_PATH`. | | `cache-hit` | `"true"` when the install was restored from cache. | +## Side effects + +- Prepends `$HOME/.opencode-oauth2-setup/bin` (Unix) or `$HOME/.opencode-oauth2-setup` (Windows) to `GITHUB_PATH` so `opencode` is callable directly in subsequent steps. +- Exports `NODE_PATH` for the active job so Node can resolve `@vymalo/opencode-oauth2` from any cwd. + +## Cache key + +`vymalo-opencode-oauth2--node--plugin--cli-@` + +Mutable specs (`latest`, `next`, `^0.2.0`) are resolved to a concrete version via `npm view` **before** they land in the key, so the cache doesn't pin a stale install indefinitely when a dist-tag moves. + ## Pinning Pin to a release tag (`@v0.2.0`) — the action ships with the plugin in the same repo, so the tag determines what gets installed by default. If you set `version:` explicitly the action uses that and only the action.yml itself is taken from the tag ref. diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 1b5a3f2..7742dd6 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -1,8 +1,8 @@ name: 'Setup @vymalo/opencode-oauth2' description: >- Install the @vymalo/opencode-oauth2 OpenCode plugin (and optionally the - opencode CLI) into the runner's global node_modules, with cross-run caching - so subsequent runs skip the install entirely. + opencode CLI) into a runner-local prefix, with cross-run caching so + subsequent runs skip the install entirely. author: 'vymalo contributors' branding: icon: 'lock' @@ -38,7 +38,7 @@ inputs: required: false default: '' cache: - description: 'Cache the global install between runs. Defaults to "true".' + description: 'Cache the install between runs. Defaults to "true".' required: false default: 'true' @@ -52,8 +52,8 @@ outputs: value: ${{ steps.resolve.outputs.opencode-version }} node-path: description: >- - Absolute path to the global node_modules directory where the plugin was - installed. Already exported as NODE_PATH for subsequent steps. + Absolute path to the node_modules directory holding the plugin. Already + exported as NODE_PATH for subsequent steps. value: ${{ steps.resolve.outputs.modules }} cache-hit: description: '"true" when the install was restored from cache.' @@ -68,60 +68,108 @@ runs: with: node-version: ${{ inputs.node-version }} + # All `${{ inputs.* }}` interpolations are routed through env: vars so the + # values never get spliced into the shell script body — that closes the + # shell-injection vector on action inputs that downstream consumers control. - name: Resolve versions and paths id: resolve shell: bash + env: + PLUGIN_SPEC: ${{ inputs.version }} + INSTALL_OPENCODE: ${{ inputs.install-opencode }} + OPENCODE_PACKAGE: ${{ inputs.opencode-package }} + OPENCODE_VERSION_SPEC: ${{ inputs.opencode-version }} run: | set -euo pipefail - plugin_spec='${{ inputs.version }}' - if [ "$plugin_spec" = "latest" ]; then - plugin_version=$(npm view @vymalo/opencode-oauth2 version) - else - plugin_version="$plugin_spec" + # Resolve any spec (dist-tag, range, exact) to a single concrete version. + # `npm view "" version` returns one line per matching version in + # ascending order; the highest match is the last line. + resolve_version() { + local spec="$1" + npm view "$spec" version 2>/dev/null | tail -1 | tr -d '[:space:]' + } + + plugin_version=$(resolve_version "@vymalo/opencode-oauth2@${PLUGIN_SPEC}") + if [ -z "$plugin_version" ]; then + echo "::error::Could not resolve @vymalo/opencode-oauth2@${PLUGIN_SPEC}" >&2 + exit 1 fi echo "plugin-version=$plugin_version" >> "$GITHUB_OUTPUT" opencode_version="" - if [ '${{ inputs.install-opencode }}' = 'true' ]; then - ov_spec='${{ inputs.opencode-version }}' - if [ "$ov_spec" = "latest" ]; then - opencode_version=$(npm view '${{ inputs.opencode-package }}' version) - else - opencode_version="$ov_spec" + if [ "$INSTALL_OPENCODE" = "true" ]; then + opencode_version=$(resolve_version "${OPENCODE_PACKAGE}@${OPENCODE_VERSION_SPEC}") + if [ -z "$opencode_version" ]; then + echo "::error::Could not resolve ${OPENCODE_PACKAGE}@${OPENCODE_VERSION_SPEC}" >&2 + exit 1 fi fi echo "opencode-version=$opencode_version" >> "$GITHUB_OUTPUT" - prefix=$(npm config get prefix) + # Use a runner-local prefix instead of the npm default so: + # - we don't need sudo for /usr/local/lib/node_modules on hosted runners, + # - the layout is identical across OSes (caching is hermetic), + # - cache restore brings back BOTH lib/node_modules AND bin/. + prefix="$HOME/.opencode-oauth2-setup" + mkdir -p "$prefix" if [ "$RUNNER_OS" = "Windows" ]; then + # npm on Windows puts modules and bin shims directly under the prefix. modules="$prefix/node_modules" + bin="$prefix" else modules="$prefix/lib/node_modules" + bin="$prefix/bin" fi echo "prefix=$prefix" >> "$GITHUB_OUTPUT" echo "modules=$modules" >> "$GITHUB_OUTPUT" + echo "bin=$bin" >> "$GITHUB_OUTPUT" + + # Node ABI matters for native deps and for npm's prefix layout. Captured + # after setup-node has run (if it ran), so the cache key reflects the + # actually-active Node. + node_version=$(node --version) + echo "node-version=$node_version" >> "$GITHUB_OUTPUT" + # Cache the entire prefix (modules + bin). Key includes: + # - OS (binaries are platform-specific), + # - Node version (ABI + npm prefix layout), + # - resolved plugin version (NOT the input spec — mutable specs like "next" + # get pinned to a concrete version above before landing in the key), + # - resolved CLI package + version (same reasoning; package name varies + # when consumers point at a fork/mirror). - name: Restore install cache if: inputs.cache == 'true' id: cache uses: actions/cache@v4 with: - path: ${{ steps.resolve.outputs.modules }} - key: vymalo-opencode-oauth2-${{ runner.os }}-plugin-${{ steps.resolve.outputs.plugin-version }}-cli-${{ steps.resolve.outputs.opencode-version || 'none' }} + path: ${{ steps.resolve.outputs.prefix }} + key: vymalo-opencode-oauth2-${{ runner.os }}-node-${{ steps.resolve.outputs.node-version }}-plugin-${{ steps.resolve.outputs.plugin-version }}-cli-${{ inputs.opencode-package }}@${{ steps.resolve.outputs.opencode-version || 'none' }} - name: Install plugin (and optionally opencode) if: steps.cache.outputs.cache-hit != 'true' shell: bash + env: + PLUGIN_VERSION: ${{ steps.resolve.outputs.plugin-version }} + INSTALL_OPENCODE: ${{ inputs.install-opencode }} + OPENCODE_PACKAGE: ${{ inputs.opencode-package }} + OPENCODE_VERSION: ${{ steps.resolve.outputs.opencode-version }} + PREFIX: ${{ steps.resolve.outputs.prefix }} run: | set -euo pipefail - pkgs=( "@vymalo/opencode-oauth2@${{ steps.resolve.outputs.plugin-version }}" ) - if [ '${{ inputs.install-opencode }}' = 'true' ]; then - pkgs+=( "${{ inputs.opencode-package }}@${{ steps.resolve.outputs.opencode-version }}" ) + pkgs=( "@vymalo/opencode-oauth2@$PLUGIN_VERSION" ) + if [ "$INSTALL_OPENCODE" = "true" ]; then + pkgs+=( "${OPENCODE_PACKAGE}@${OPENCODE_VERSION}" ) fi - npm install -g --no-audit --no-fund "${pkgs[@]}" + npm install -g --prefix "$PREFIX" --no-audit --no-fund "${pkgs[@]}" - - name: Export NODE_PATH + - name: Export PATH and NODE_PATH shell: bash + env: + MODULES: ${{ steps.resolve.outputs.modules }} + BIN: ${{ steps.resolve.outputs.bin }} run: | - echo "NODE_PATH=${{ steps.resolve.outputs.modules }}" >> "$GITHUB_ENV" + echo "NODE_PATH=$MODULES" >> "$GITHUB_ENV" + # Prepend the prefix's bin to PATH so `opencode` (and any peer CLI) is + # invocable in subsequent steps without a wrapper. + echo "$BIN" >> "$GITHUB_PATH" diff --git a/.github/workflows/opencode-run.yml b/.github/workflows/opencode-run.yml index 20beff6..4377084 100644 --- a/.github/workflows/opencode-run.yml +++ b/.github/workflows/opencode-run.yml @@ -75,25 +75,68 @@ jobs: - name: Checkout caller repo uses: actions/checkout@v4 - # The reusable workflow lives in vymalo/opencode-oauth2, so we also need - # to check out THIS repo to a side path to load the composite action from - # the same ref the caller pinned. `github.job_workflow_sha` is the SHA of - # this reusable workflow file's commit — guarantees action + workflow stay - # in lockstep. - - name: Checkout opencode-oauth2 (for the setup action) - uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 with: - repository: vymalo/opencode-oauth2 - ref: ${{ github.job_workflow_sha }} - path: .opencode-oauth2-action + node-version: ${{ inputs.node-version }} + + # Resolve any mutable npm spec (dist-tag, range) to a concrete version so + # the cache key below pins to a stable identity. Inputs are routed via + # env: to keep them out of the shell body (no injection vector). + - name: Resolve plugin + CLI versions + id: resolve + env: + PLUGIN_SPEC: ${{ inputs.plugin-version }} + OPENCODE_SPEC: ${{ inputs.opencode-version }} + run: | + set -euo pipefail + resolve_version() { + npm view "$1" version 2>/dev/null | tail -1 | tr -d '[:space:]' + } + plugin_version=$(resolve_version "@vymalo/opencode-oauth2@${PLUGIN_SPEC:-latest}") + opencode_version=$(resolve_version "opencode@${OPENCODE_SPEC:-latest}") + if [ -z "$plugin_version" ] || [ -z "$opencode_version" ]; then + echo "::error::Could not resolve plugin or CLI version" >&2 + exit 1 + fi + prefix="$HOME/.opencode-oauth2-setup" + mkdir -p "$prefix" + node_version=$(node --version) + { + echo "plugin-version=$plugin_version" + echo "opencode-version=$opencode_version" + echo "prefix=$prefix" + echo "modules=$prefix/lib/node_modules" + echo "bin=$prefix/bin" + echo "node-version=$node_version" + } >> "$GITHUB_OUTPUT" - - name: Install opencode + plugin (cached) - uses: ./.opencode-oauth2-action/.github/actions/setup + - name: Restore install cache + id: cache + uses: actions/cache@v4 with: - node-version: ${{ inputs.node-version }} - version: ${{ inputs.plugin-version || 'latest' }} - install-opencode: 'true' - opencode-version: ${{ inputs.opencode-version || 'latest' }} + path: ${{ steps.resolve.outputs.prefix }} + key: vymalo-opencode-oauth2-${{ runner.os }}-node-${{ steps.resolve.outputs.node-version }}-plugin-${{ steps.resolve.outputs.plugin-version }}-cli-opencode@${{ steps.resolve.outputs.opencode-version }} + + - name: Install opencode + plugin + if: steps.cache.outputs.cache-hit != 'true' + env: + PLUGIN_VERSION: ${{ steps.resolve.outputs.plugin-version }} + OPENCODE_VERSION: ${{ steps.resolve.outputs.opencode-version }} + PREFIX: ${{ steps.resolve.outputs.prefix }} + run: | + set -euo pipefail + npm install -g --prefix "$PREFIX" --no-audit --no-fund \ + "opencode@$OPENCODE_VERSION" \ + "@vymalo/opencode-oauth2@$PLUGIN_VERSION" + + - name: Export PATH and NODE_PATH + env: + MODULES: ${{ steps.resolve.outputs.modules }} + BIN: ${{ steps.resolve.outputs.bin }} + run: | + echo "NODE_PATH=$MODULES" >> "$GITHUB_ENV" + echo "$BIN" >> "$GITHUB_PATH" - name: Verify opencode config exists run: |