Skip to content

feat(ci): add reusable composite action for plugin install#15

Merged
stephane-segning merged 2 commits into
mainfrom
claude/charming-goldwasser-200471
May 25, 2026
Merged

feat(ci): add reusable composite action for plugin install#15
stephane-segning merged 2 commits into
mainfrom
claude/charming-goldwasser-200471

Conversation

@stephane-segning
Copy link
Copy Markdown
Contributor

Summary

  • Adds .github/actions/setup — a composite GitHub Action that installs @vymalo/opencode-oauth2 (and optionally the opencode CLI) into the runner's global node_modules, with actions/cache keyed on version + OS so subsequent runs skip the install entirely.
  • Exports NODE_PATH after install so opencode resolves the plugin no matter what cwd it's invoked from.
  • Updates the package README's GitHub Actions section to recommend the composite action instead of an ad-hoc npm install -g step.

Why

Consumers running OpenCode in CI (e.g. for an AI-assisted job, code review bot, etc.) shouldn't have to pay for a full pnpm install of their own repo just to make the plugin available. The composite action does the minimum: one cached npm install -g of the plugin (and optionally CLI), then is a no-op on every subsequent run for the same version.

Usage

- uses: vymalo/opencode-oauth2/.github/actions/setup@v0.2.0
  with:
    node-version: '22'
    install-opencode: 'true'

Inputs: version, install-opencode, opencode-package, opencode-version, node-version, cache.
Outputs: version, opencode-version, node-path, cache-hit.

Full reference in .github/actions/setup/README.md.

Reviewer notes

  • The action becomes consumable as @v0.2.0 only once that tag exists on the default branch. Until then, callers can pin to @main or a SHA. Might be worth a one-line note in publish.yml on the next release.
  • latest version resolution uses npm view, so air-gapped runners would need to pin an explicit version:.
  • The cache key includes runner.os, plugin version, and CLI version (or the literal none when install-opencode=false), so toggling install-opencode doesn't poison either cache entry.

Test plan

  • Tag a release (or pin a consumer workflow at the merge SHA) and confirm uses: vymalo/opencode-oauth2/.github/actions/setup@<ref> resolves and runs.
  • First run installs and populates the cache; confirm cache-hit output is empty/false and opencode run succeeds.
  • Second run on same version hits the cache; confirm cache-hit=true and the install step is skipped.
  • Flip install-opencode between runs and confirm both cache variants coexist.

🤖 Generated with Claude Code

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new composite GitHub Action to facilitate the setup of the @vymalo/opencode-oauth2 plugin and the opencode CLI in CI environments, featuring cross-run caching. The review feedback identifies several critical security and functional issues: direct interpolation of GitHub Action inputs into shell scripts creates shell injection vulnerabilities, and the current caching strategy for global npm installs is incomplete as it misses binary symlinks. Recommendations include passing inputs via environment variables, using a dedicated local installation prefix to avoid permission issues, and ensuring the bin directory is added to the system PATH so that the CLI tools can be invoked correctly.

Comment on lines +71 to +103
- 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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current implementation is vulnerable to shell injection because it interpolates GitHub Action inputs directly into the bash script. Additionally, using the default global npm prefix can lead to permission issues on some runners and makes caching unreliable because the prefix location and structure can vary.

Recommendation:

  1. Pass inputs as environment variables to the shell script.
  2. Use a dedicated local directory for the installation. This ensures a consistent structure for caching and avoids the need for sudo on runners where the default prefix is system-owned.
    - 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

        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 [ "$INSTALL_OPENCODE" = "true" ]; then
          if [ "$OPENCODE_VERSION_SPEC" = "latest" ]; then
            opencode_version=$(npm view "$OPENCODE_PACKAGE" version)
          else
            opencode_version="$OPENCODE_VERSION_SPEC"
          fi
        fi
        echo "opencode-version=$opencode_version" >> "$GITHUB_OUTPUT"

        prefix="$HOME/.opencode-setup"
        mkdir -p "$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"

Comment thread .github/actions/setup/action.yml Outdated
id: cache
uses: actions/cache@v4
with:
path: ${{ steps.resolve.outputs.modules }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Caching only the node_modules directory is insufficient for global tools. On Unix-like systems, npm install -g places executable symlinks in the bin directory, which is outside of node_modules. On a cache hit, the install step is skipped, and these binaries will be missing from the runner, causing the opencode command to fail.

Recommendation: Cache the entire custom prefix directory to ensure both modules and binaries are preserved.

        path: ${{ steps.resolve.outputs.prefix }}

Comment thread .github/actions/setup/action.yml Outdated
Comment on lines +113 to +122
- 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[@]}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

This step is also vulnerable to shell injection via inputs.opencode-package. Additionally, it should use the custom prefix defined in the resolve step to ensure the installation goes into the cached directory.

    - 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@$PLUGIN_VERSION" )
        if [ "$INSTALL_OPENCODE" = "true" ]; then
          pkgs+=( "$OPENCODE_PACKAGE@$OPENCODE_VERSION" )
        fi
        npm install -g --prefix "$PREFIX" --no-audit --no-fund "${pkgs[@]}"

Comment thread .github/actions/setup/action.yml Outdated
Comment on lines +124 to +127
- name: Export NODE_PATH
shell: bash
run: |
echo "NODE_PATH=${{ steps.resolve.outputs.modules }}" >> "$GITHUB_ENV"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

When using a custom installation prefix, you must add the corresponding bin directory to the system PATH so that the installed CLI tools can be invoked directly. Also, it's safer to use environment variables for the path values.

    - name: Export PATH and NODE_PATH
      shell: bash
      env:
        MODULES: ${{ steps.resolve.outputs.modules }}
        PREFIX: ${{ steps.resolve.outputs.prefix }}
      run: |
        echo "NODE_PATH=$MODULES" >> "$GITHUB_ENV"
        if [ "$RUNNER_OS" = "Windows" ]; then
          echo "$PREFIX" >> "$GITHUB_PATH"
        else
          echo "$PREFIX/bin" >> "$GITHUB_PATH"
        fi

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cdbbb53e76

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/actions/setup/action.yml Outdated
Comment on lines +110 to +111
path: ${{ steps.resolve.outputs.modules }}
key: vymalo-opencode-oauth2-${{ runner.os }}-plugin-${{ steps.resolve.outputs.plugin-version }}-cli-${{ steps.resolve.outputs.opencode-version || 'none' }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Cache global bin directory for CLI installs

When install-opencode=true, a cache hit skips npm install -g, but the cache only restores node_modules. Global npm installs place the executable shim in the global prefix bin directory, so on a fresh runner the opencode command can be missing even though modules were restored. This breaks the documented flow on subsequent runs with the same key (opencode run fails with command-not-found).

Useful? React with 👍 / 👎.

Comment thread .github/actions/setup/action.yml Outdated
Comment on lines +80 to +82
else
plugin_version="$plugin_spec"
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve mutable npm specs before deriving cache key

For non-latest inputs, the action stores the raw spec string (for example next or ^0.2.0) as plugin-version, and that value is used in the cache key. Because dist-tags/ranges can move over time while the key stays the same, the cache can pin an old install indefinitely and prevent newer matching releases from ever being installed.

Useful? React with 👍 / 👎.

Comment thread .github/actions/setup/action.yml Outdated
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' }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include opencode package name in cache key

The cache key only varies by opencode-version, but the action also allows overriding opencode-package. If one run uses the default package and another uses a fork/mirror with the same version string, they collide on the same cache entry and the install step is skipped on hit, leaving the previously cached CLI package in place instead of the requested one.

Useful? React with 👍 / 👎.

Comment thread .github/actions/setup/action.yml Outdated
Comment on lines +90 to +92
else
opencode_version="$ov_spec"
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve mutable CLI specs before deriving cache key

When install-opencode=true and opencode-version is a moving spec (for example next or ^1.2.0), the action writes that raw spec into opencode-version and reuses it in the cache key. Since the spec text is stable while its resolved version can change, later runs can keep restoring an old CLI from cache and never pick up newer matching releases.

Useful? React with 👍 / 👎.

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) <noreply@anthropic.com>
@stephane-segning stephane-segning force-pushed the claude/charming-goldwasser-200471 branch from cdbbb53 to 0e5fb17 Compare May 25, 2026 01:14
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e5fb17bdf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/opencode-run.yml Outdated
with:
node-version: ${{ inputs.node-version }}
repository: vymalo/opencode-oauth2
ref: ${{ github.job_workflow_sha }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use job.workflow_sha for action checkout ref

github.job_workflow_sha is not a valid GitHub context property, so this expression resolves empty and actions/checkout falls back to the repository’s default branch. In a reusable workflow pinned by callers to a tag/SHA, that means the setup action is no longer loaded from the same pinned revision, causing drift (and potential breakage) between the workflow and action code despite the lockstep intent in this section.

Useful? React with 👍 / 👎.

Comment thread .github/actions/setup/action.yml Outdated
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' }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add Node runtime to setup-action cache key

The cache key ignores the Node runtime even though this action can switch Node via inputs.node-version, so runs on different Node versions can hit the same cache entry and skip installation. Because the global npm prefix path is Node-install specific on hosted runners, a cross-version cache hit can restore a different runtime’s global install while NODE_PATH points to the current runtime path, leaving opencode/plugin resolution broken or stale in matrix jobs and after Node patch upgrades.

Useful? React with 👍 / 👎.

Comment thread .github/workflows/opencode-run.yml Outdated
uses: actions/checkout@v4
with:
node-version: ${{ inputs.node-version }}
repository: vymalo/opencode-oauth2
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve action repo from workflow context

Hard-coding repository: vymalo/opencode-oauth2 means this reusable workflow fetches the setup action from that upstream repo even when the workflow itself is called from a fork or after a repo rename/transfer. In those cases the workflow and action are no longer sourced from the same pinned reference, which defeats the lockstep guarantee and can run mismatched code.

Useful? React with 👍 / 👎.

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) <noreply@anthropic.com>
@stephane-segning stephane-segning merged commit f35cebb into main May 25, 2026
3 checks passed
@stephane-segning stephane-segning deleted the claude/charming-goldwasser-200471 branch May 25, 2026 01:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant