-
Notifications
You must be signed in to change notification settings - Fork 3
ci: add manual plugin dependency update workflow #156
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
4 commits
Select commit
Hold shift + click to select a range
2424879
ci: add manual plugin dependency update workflow
mynameistito 5ea1057
fix: address plugin update workflow review
mynameistito 9177f51
fix: address remaining plugin update review findings
mynameistito b513692
test: run script checks and retain label permissions
mynameistito 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
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,88 @@ | ||
| name: Update @opencode/plugin | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| concurrency: | ||
| group: update-opencode-plugin | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| update: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout main | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: main | ||
| persist-credentials: false | ||
|
|
||
| - name: Setup Bun | ||
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | ||
| with: | ||
| bun-version: 1.4.2 | ||
|
|
||
| - name: Prepare update branch | ||
| id: branch | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| set -euo pipefail | ||
| gh auth setup-git | ||
| branch_sha=$(git ls-remote --heads origin refs/heads/update-opencode-plugin | cut -f1) | ||
| if [ -n "$branch_sha" ]; then | ||
| git fetch origin update-opencode-plugin | ||
| fi | ||
| git checkout -B update-opencode-plugin main | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
| echo "branch_sha=$branch_sha" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Check npm for the latest version | ||
| id: latest | ||
| run: bun scripts/update-opencode-plugin.ts | ||
|
|
||
| - name: Install dependencies and update lockfile | ||
| if: steps.latest.outputs.changed == 'true' | ||
| run: bun install | ||
|
|
||
| - name: Add plugin Changesets | ||
| if: steps.latest.outputs.changed == 'true' | ||
| env: | ||
| VERSION: ${{ steps.latest.outputs.version }} | ||
| run: | | ||
| bun run changeset-add -- usage-limits patch "Update @opencode/plugin to $VERSION" | ||
| bun run changeset-add -- force-input patch "Update @opencode/plugin to $VERSION" | ||
|
|
||
| - name: Commit and push update | ||
| if: steps.latest.outputs.changed == 'true' | ||
| env: | ||
| BRANCH_SHA: ${{ steps.branch.outputs.branch_sha }} | ||
| GH_TOKEN: ${{ github.token }} | ||
| VERSION: ${{ steps.latest.outputs.version }} | ||
| run: | | ||
| gh auth setup-git | ||
| git add package.json bun.lock .changeset | ||
| git commit -m "chore(deps): update @opencode/plugin to $VERSION" | ||
| if [ -n "$BRANCH_SHA" ]; then | ||
| git push --force-with-lease="refs/heads/update-opencode-plugin:$BRANCH_SHA" origin update-opencode-plugin | ||
| else | ||
| git push --set-upstream origin update-opencode-plugin | ||
| fi | ||
|
|
||
| - name: Open or update pull request | ||
| if: steps.latest.outputs.changed == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| VERSION: ${{ steps.latest.outputs.version }} | ||
| run: | | ||
| pr_number=$(gh pr list --head update-opencode-plugin --base main --json number --jq 'if length > 0 then .[0].number else empty end') | ||
| if [ -n "$pr_number" ]; then | ||
| gh pr edit "$pr_number" --title "chore(deps): update @opencode/plugin" --body "Updates the shared @opencode/plugin dependency to $VERSION and includes patch Changesets for usage-limits and force-input." | ||
| else | ||
| gh pr create --base main --head update-opencode-plugin --title "chore(deps): update @opencode/plugin" --body "Updates the shared @opencode/plugin dependency to $VERSION and includes patch Changesets for usage-limits and force-input." | ||
| fi |
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
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,28 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { isNewerSemVer, parseSerializedSemVer } from "../semver"; | ||
|
|
||
| describe("Semantic version comparison", () => { | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| it("returns true only when the candidate is newer", () => { | ||
| expect(isNewerSemVer("2.0.22", "2.0.21")).toBeTruthy(); | ||
| expect(isNewerSemVer("2.0.21", "2.0.21")).toBeFalsy(); | ||
| expect(isNewerSemVer("2.0.20", "2.0.21")).toBeFalsy(); | ||
| }); | ||
|
|
||
| it("compares prereleases according to SemVer precedence", () => { | ||
| expect(isNewerSemVer("1.0.0", "1.0.0-rc.2")).toBeTruthy(); | ||
| expect(isNewerSemVer("1.0.0-rc.10", "1.0.0-rc.2")).toBeTruthy(); | ||
| expect(isNewerSemVer("1.0.0-rc.2", "1.0.0-rc.10")).toBeFalsy(); | ||
| }); | ||
|
|
||
| it("ignores build metadata when comparing precedence", () => { | ||
| expect(isNewerSemVer("1.0.0+build.2", "1.0.0+build.1")).toBeFalsy(); | ||
| }); | ||
|
|
||
| it("rejects malformed versions", () => { | ||
| expect(() => parseSerializedSemVer('"1.0"', "test")).toThrow(TypeError); | ||
| expect(() => parseSerializedSemVer('"1.0.0-01"', "test")).toThrow( | ||
| TypeError | ||
| ); | ||
| }); | ||
| }); | ||
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,126 @@ | ||
| const serializedVersionPattern = | ||
| /^"(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?"$/u; | ||
|
|
||
| interface ParsedVersion { | ||
| major: bigint; | ||
| minor: bigint; | ||
| patch: bigint; | ||
| prerelease: string[]; | ||
| value: string; | ||
| } | ||
|
|
||
| export const parseSerializedSemVer = ( | ||
| serialized: string, | ||
| source: string | ||
| ): ParsedVersion => { | ||
| const match = serializedVersionPattern.exec(serialized); | ||
|
|
||
| if (!match) { | ||
| throw new TypeError(`${source} did not contain a valid package version`); | ||
| } | ||
|
|
||
| const { major, minor, patch, prerelease } = match.groups ?? {}; | ||
|
|
||
| if (!major || !minor || !patch) { | ||
| throw new TypeError(`${source} did not contain a valid package version`); | ||
| } | ||
|
|
||
| return { | ||
| major: BigInt(major), | ||
| minor: BigInt(minor), | ||
| patch: BigInt(patch), | ||
| prerelease: prerelease?.split(".") ?? [], | ||
| value: serialized.slice(1, -1), | ||
| }; | ||
| }; | ||
|
|
||
| const compareBigInts = (left: bigint, right: bigint): number => { | ||
| if (left === right) { | ||
| return 0; | ||
| } | ||
|
|
||
| return left > right ? 1 : -1; | ||
| }; | ||
|
|
||
| const compareCoreVersions = ( | ||
| left: ParsedVersion, | ||
| right: ParsedVersion | ||
| ): number => { | ||
| for (const key of ["major", "minor", "patch"] as const) { | ||
| if (left[key] !== right[key]) { | ||
| return compareBigInts(left[key], right[key]); | ||
| } | ||
| } | ||
|
|
||
| return 0; | ||
| }; | ||
|
|
||
| const comparePrereleaseIdentifier = (left: string, right: string): number => { | ||
| const leftIsNumeric = /^\d+$/u.test(left); | ||
| const rightIsNumeric = /^\d+$/u.test(right); | ||
|
|
||
| if (leftIsNumeric && rightIsNumeric) { | ||
| return compareBigInts(BigInt(left), BigInt(right)); | ||
| } | ||
|
|
||
| if (leftIsNumeric !== rightIsNumeric) { | ||
| return leftIsNumeric ? -1 : 1; | ||
| } | ||
|
|
||
| if (left === right) { | ||
| return 0; | ||
| } | ||
|
|
||
| return left > right ? 1 : -1; | ||
| }; | ||
|
|
||
| const comparePrerelease = (left: string[], right: string[]): number => { | ||
| for (const [index, leftIdentifier] of left.entries()) { | ||
| const rightIdentifier = right[index]; | ||
|
|
||
| if (rightIdentifier === undefined) { | ||
| return 1; | ||
| } | ||
|
|
||
| const comparison = comparePrereleaseIdentifier( | ||
| leftIdentifier, | ||
| rightIdentifier | ||
| ); | ||
|
|
||
| if (comparison !== 0) { | ||
| return comparison; | ||
| } | ||
| } | ||
|
|
||
| return left.length === right.length ? 0 : -1; | ||
| }; | ||
|
|
||
| const compareVersions = (left: ParsedVersion, right: ParsedVersion): number => { | ||
| const coreComparison = compareCoreVersions(left, right); | ||
|
|
||
| if (coreComparison !== 0) { | ||
| return coreComparison; | ||
| } | ||
|
|
||
| const leftIsRelease = left.prerelease.length === 0; | ||
| const rightIsRelease = right.prerelease.length === 0; | ||
|
|
||
| if (leftIsRelease !== rightIsRelease) { | ||
| return leftIsRelease ? 1 : -1; | ||
| } | ||
|
|
||
| return comparePrerelease(left.prerelease, right.prerelease); | ||
| }; | ||
|
|
||
| export const isNewerSemVer = (candidate: string, current: string): boolean => { | ||
| const parsedCandidate = parseSerializedSemVer( | ||
| JSON.stringify(candidate) ?? "", | ||
| "Candidate version" | ||
| ); | ||
| const parsedCurrent = parseSerializedSemVer( | ||
| JSON.stringify(current) ?? "", | ||
| "Current version" | ||
| ); | ||
|
|
||
| return compareVersions(parsedCandidate, parsedCurrent) > 0; | ||
| }; |
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,79 @@ | ||
| import { appendFile, readFile, writeFile } from "node:fs/promises"; | ||
|
|
||
| import { isNewerSemVer, parseSerializedSemVer } from "./semver"; | ||
|
|
||
| const packageJsonPath = new URL("../package.json", import.meta.url); | ||
| const packageJson: unknown = JSON.parse( | ||
| await readFile(packageJsonPath, "utf-8") | ||
| ); | ||
|
|
||
| if ( | ||
| !(packageJson instanceof Object) || | ||
| Array.isArray(packageJson) || | ||
| !("catalog" in packageJson) | ||
| ) { | ||
| throw new TypeError("Root package.json must contain an object"); | ||
| } | ||
|
|
||
| const { catalog } = packageJson; | ||
|
|
||
| if ( | ||
| !(catalog instanceof Object) || | ||
| Array.isArray(catalog) || | ||
| !("@opencode/plugin" in catalog) | ||
| ) { | ||
| throw new TypeError("Root package.json must define a catalog object"); | ||
| } | ||
|
|
||
| const currentVersion = parseSerializedSemVer( | ||
| JSON.stringify(catalog["@opencode/plugin"]) ?? "", | ||
| "Root package catalog" | ||
| ); | ||
|
|
||
| const response = await fetch( | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| "https://registry.npmjs.org/@opencode%2fplugin/latest", | ||
| { signal: AbortSignal.timeout(15_000) } | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error( | ||
| `npm registry returned ${response.status} for @opencode/plugin` | ||
| ); | ||
| } | ||
|
|
||
| const metadata: unknown = await response.json(); | ||
|
|
||
| if ( | ||
| !(metadata instanceof Object) || | ||
| Array.isArray(metadata) || | ||
| !("version" in metadata) | ||
| ) { | ||
| throw new TypeError("npm registry response must contain package metadata"); | ||
| } | ||
|
|
||
| const latestVersion = parseSerializedSemVer( | ||
| JSON.stringify(metadata.version) ?? "", | ||
| "npm registry response" | ||
| ); | ||
|
|
||
| const changed = isNewerSemVer(latestVersion.value, currentVersion.value); | ||
|
|
||
| if (changed) { | ||
| Reflect.set(catalog, "@opencode/plugin", latestVersion.value); | ||
| await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); | ||
| } | ||
|
|
||
| const outputPath = process.env.GITHUB_OUTPUT; | ||
|
|
||
| if (outputPath) { | ||
| await appendFile( | ||
| outputPath, | ||
| `changed=${changed}\nversion=${latestVersion.value}\n` | ||
| ); | ||
| } | ||
|
|
||
| console.log( | ||
| changed | ||
| ? `Updated @opencode/plugin from ${currentVersion.value} to ${latestVersion.value}` | ||
| : `@opencode/plugin is already current at ${currentVersion.value}` | ||
| ); | ||
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.
Uh oh!
There was an error while loading. Please reload this page.