-
Notifications
You must be signed in to change notification settings - Fork 0
feat(ci): add reusable composite action for plugin install #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| # `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 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 --prefix <local>` 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 both `lib/node_modules` and `bin/` (the CLI shims) are restored from the cache — `opencode` is invocable with no install step. | ||
|
|
||
| ## 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 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-<runner-os>-node-<node-version>-plugin-<resolved-plugin-version>-cli-<opencode-package>@<resolved-cli-version|none>` | ||
|
|
||
| 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. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| name: 'Setup @vymalo/opencode-oauth2' | ||
| description: >- | ||
| Install the @vymalo/opencode-oauth2 OpenCode plugin (and optionally the | ||
| opencode CLI) into a runner-local prefix, 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 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 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.' | ||
| 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 }} | ||
|
|
||
| # 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 | ||
|
|
||
| # Resolve any spec (dist-tag, range, exact) to a single concrete version. | ||
| # `npm view "<spec>" 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 [ "$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" | ||
|
|
||
| # 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.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@$PLUGIN_VERSION" ) | ||
| if [ "$INSTALL_OPENCODE" = "true" ]; then | ||
| pkgs+=( "${OPENCODE_PACKAGE}@${OPENCODE_VERSION}" ) | ||
| fi | ||
| npm install -g --prefix "$PREFIX" --no-audit --no-fund "${pkgs[@]}" | ||
|
|
||
| - name: Export PATH and NODE_PATH | ||
| shell: bash | ||
| env: | ||
| MODULES: ${{ steps.resolve.outputs.modules }} | ||
| BIN: ${{ steps.resolve.outputs.bin }} | ||
| run: | | ||
| 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" | ||
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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
sudoon runners where the default prefix is system-owned.