Update omp dependency #402
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: Update omp dependency | |
| # The upstream repository cannot dispatch a workflow in this repository, so the | |
| # hourly check reads its latest stable release. workflow_dispatch is useful for | |
| # validating a release immediately after it is published. | |
| on: | |
| schedule: | |
| - cron: "17 * * * *" | |
| workflow_dispatch: | |
| pull_request: | |
| types: | |
| - closed | |
| permissions: | |
| actions: write | |
| contents: write | |
| pull-requests: write | |
| concurrency: | |
| group: update-omp | |
| cancel-in-progress: false | |
| jobs: | |
| update: | |
| name: Open omp dependency PR | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check out main | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| registry-url: https://registry.npmjs.org | |
| - name: Set up Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| # Must satisfy engines.bun (>=1.3.14): the omp SDK is published as | |
| # TypeScript importing `bun:` builtins, so verifying the bump on an | |
| # older Bun proves nothing about the release this repo actually ships. | |
| bun-version: "1.3.14" | |
| - name: Read latest upstream release | |
| id: release | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ github.token }} | |
| script: | | |
| const { data } = await github.rest.repos.getLatestRelease({ | |
| owner: "can1357", | |
| repo: "oh-my-pi", | |
| }); | |
| const version = String(data.tag_name || "").replace(/^v/, ""); | |
| if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) { | |
| core.setFailed(`Unexpected upstream release tag: ${data.tag_name}`); | |
| return; | |
| } | |
| core.setOutput("version", version); | |
| core.setOutput("tag", data.tag_name); | |
| core.setOutput("url", data.html_url); | |
| - name: Skip when the dependency is current | |
| id: manifest | |
| env: | |
| OMP_VERSION: ${{ steps.release.outputs.version }} | |
| shell: bash | |
| run: | | |
| current="$(node -p "require('./package.json').dependencies['@oh-my-pi/pi-coding-agent']" | sed -E 's/^[^0-9]*//')" | |
| echo "current=$current" >> "$GITHUB_OUTPUT" | |
| if [[ "$current" == "$OMP_VERSION" ]]; then | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| echo "omp $OMP_VERSION is already declared in package.json" | |
| exit 0 | |
| fi | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| - name: Wait for the matching npm package | |
| id: npm | |
| if: steps.manifest.outputs.changed == 'true' | |
| env: | |
| OMP_VERSION: ${{ steps.release.outputs.version }} | |
| shell: bash | |
| run: | | |
| for attempt in {1..10}; do | |
| published="$(npm view "@oh-my-pi/pi-coding-agent@${OMP_VERSION}" version --silent 2>/dev/null || true)" | |
| if [[ "$published" == "$OMP_VERSION" ]]; then | |
| echo "published=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "@oh-my-pi/pi-coding-agent@$OMP_VERSION is not on npm yet (attempt $attempt/10)" | |
| sleep 30 | |
| done | |
| # Upstream routinely tags minutes before the npm publish lands. The | |
| # hourly schedule is the retry, so a miss here is a "come back later", | |
| # not a failure worth reddening every run until someone notices. | |
| echo "published=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::omp $OMP_VERSION is tagged upstream but not on npm yet; the next scheduled run will retry." | |
| - name: Decide whether to prepare the update | |
| id: plan | |
| shell: bash | |
| env: | |
| CHANGED: ${{ steps.manifest.outputs.changed }} | |
| PUBLISHED: ${{ steps.npm.outputs.published }} | |
| run: | | |
| if [[ "$CHANGED" == "true" && "$PUBLISHED" == "true" ]]; then | |
| echo "proceed=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "proceed=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Update omp dependency ranges | |
| if: steps.plan.outputs.proceed == 'true' | |
| env: | |
| OMP_VERSION: ${{ steps.release.outputs.version }} | |
| run: | | |
| node <<'NODE' | |
| const fs = require('node:fs'); | |
| const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
| const names = [ | |
| '@oh-my-pi/pi-agent-core', | |
| '@oh-my-pi/pi-ai', | |
| '@oh-my-pi/pi-coding-agent', | |
| '@oh-my-pi/pi-tui', | |
| '@oh-my-pi/pi-utils', | |
| ]; | |
| for (const name of names) { | |
| if (!packageJson.dependencies?.[name]) throw new Error(`Missing dependency ${name}`); | |
| packageJson.dependencies[name] = `~${process.env.OMP_VERSION}`; | |
| } | |
| fs.writeFileSync('package.json', `${JSON.stringify(packageJson, null, 2)}\n`); | |
| NODE | |
| - name: Bump the omp-web version | |
| id: package | |
| if: steps.plan.outputs.proceed == 'true' | |
| env: | |
| OMP_VERSION: ${{ steps.release.outputs.version }} | |
| OMP_PREVIOUS: ${{ steps.manifest.outputs.current }} | |
| run: | | |
| node <<'NODE' | |
| const fs = require('node:fs'); | |
| const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
| const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(packageJson.version); | |
| if (!match) { | |
| throw new Error(`Unexpected omp-web version: ${packageJson.version}`); | |
| } | |
| const [major, minor, patch] = match.slice(1).map(Number); | |
| // omp-web serves omp's own SDK in-process, so a new omp major moves | |
| // the surface under omp-web's users too. That earns a minor bump; a | |
| // routine refresh within the same major stays a patch. | |
| const ompMajor = (version) => /^(\d+)\./.exec(version || '')?.[1]; | |
| const nextMajor = ompMajor(process.env.OMP_VERSION); | |
| const previousMajor = ompMajor(process.env.OMP_PREVIOUS); | |
| if (!nextMajor) throw new Error(`Unexpected omp version: ${process.env.OMP_VERSION}`); | |
| const nextVersion = nextMajor === previousMajor | |
| ? `${major}.${minor}.${patch + 1}` | |
| : `${major}.${minor + 1}.0`; | |
| packageJson.version = nextVersion; | |
| fs.writeFileSync('package.json', `${JSON.stringify(packageJson, null, 2)}\n`); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${nextVersion}\n`); | |
| NODE | |
| - name: Refresh Bun lockfile | |
| if: steps.plan.outputs.proceed == 'true' | |
| run: bun install --lockfile-only | |
| # A breaking upstream release is the normal reason this fails, and that is | |
| # exactly when the update is most worth surfacing. Record the outcome | |
| # instead of aborting, so the run still opens a PR carrying the bump. | |
| - name: Verify the updated dependency graph | |
| id: verify | |
| if: steps.plan.outputs.proceed == 'true' | |
| continue-on-error: true | |
| run: | | |
| bun install --frozen-lockfile | |
| bun run typecheck | |
| bun test | |
| - name: Report the verification outcome | |
| id: report | |
| if: steps.plan.outputs.proceed == 'true' | |
| shell: bash | |
| env: | |
| OMP_VERSION: ${{ steps.release.outputs.version }} | |
| VERIFY_OUTCOME: ${{ steps.verify.outcome }} | |
| run: | | |
| if [[ "$VERIFY_OUTCOME" == "success" ]]; then | |
| note="The workflow refreshed \`bun.lock\`, then ran \`bun run typecheck\` and \`bun test\` against the new SDK. Both passed." | |
| echo "draft=false" >> "$GITHUB_OUTPUT" | |
| else | |
| note=":warning: \`bun run typecheck\` or \`bun test\` failed against omp \`$OMP_VERSION\`, so this PR is a **draft**: the release needs source changes in omp-web before it can merge. The failing output is in [the workflow run](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})." | |
| echo "draft=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| { | |
| echo "note<<EOF" | |
| echo "$note" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "$note" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Open or update the dependency PR | |
| if: steps.plan.outputs.proceed == 'true' | |
| uses: peter-evans/create-pull-request@v7 | |
| with: | |
| token: ${{ github.token }} | |
| branch: automation/omp-${{ steps.release.outputs.version }} | |
| delete-branch: true | |
| draft: ${{ steps.report.outputs.draft }} | |
| commit-message: "chore: update omp to v${{ steps.release.outputs.version }}" | |
| title: "chore: update omp to v${{ steps.release.outputs.version }}" | |
| body: | | |
| Updates all `@oh-my-pi/*` runtime packages to `~${{ steps.release.outputs.version }}`. | |
| Upstream release: [${{ steps.release.outputs.tag }}](${{ steps.release.outputs.url }}) | |
| When merged, this PR will release `omp-web@${{ steps.package.outputs.version }}` with a note for `oh-my-pi v${{ steps.release.outputs.version }}`. | |
| ${{ steps.report.outputs.note }} | |
| release: | |
| name: Release omp-web | |
| if: >- | |
| github.event_name == 'pull_request' && | |
| github.event.pull_request.merged == true && | |
| github.event.pull_request.base.ref == github.event.repository.default_branch && | |
| github.event.pull_request.head.repo.full_name == github.repository && | |
| startsWith(github.event.pull_request.head.ref, 'automation/omp-') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check out merged main | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.merge_commit_sha }} | |
| fetch-depth: 1 | |
| - name: Read release metadata | |
| id: metadata | |
| shell: bash | |
| run: | | |
| node <<'NODE' | |
| const fs = require('node:fs'); | |
| const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
| const packageVersion = packageJson.version; | |
| if (!/^\d+\.\d+\.\d+$/.test(packageVersion)) { | |
| throw new Error(`Unexpected omp-web version: ${packageVersion}`); | |
| } | |
| const names = [ | |
| '@oh-my-pi/pi-agent-core', | |
| '@oh-my-pi/pi-ai', | |
| '@oh-my-pi/pi-coding-agent', | |
| '@oh-my-pi/pi-tui', | |
| '@oh-my-pi/pi-utils', | |
| ]; | |
| const versions = names.map((name) => { | |
| const range = packageJson.dependencies?.[name]; | |
| const match = /^~(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(range || ''); | |
| if (!match) { | |
| throw new Error(`Unexpected ${name} range: ${range}`); | |
| } | |
| return match[1]; | |
| }); | |
| if (new Set(versions).size !== 1) { | |
| throw new Error(`@oh-my-pi/* packages do not share one version: ${versions.join(', ')}`); | |
| } | |
| fs.appendFileSync( | |
| process.env.GITHUB_OUTPUT, | |
| `package_version=${packageVersion}\nomp_version=${versions[0]}\n`, | |
| ); | |
| NODE | |
| - name: Create GitHub release | |
| id: github_release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PACKAGE_VERSION: ${{ steps.metadata.outputs.package_version }} | |
| OMP_VERSION: ${{ steps.metadata.outputs.omp_version }} | |
| TARGET_SHA: ${{ github.event.pull_request.merge_commit_sha }} | |
| run: | | |
| tag="v${PACKAGE_VERSION}" | |
| if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| echo "Release $tag already exists" | |
| echo "created=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| gh release create "$tag" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --target "$TARGET_SHA" \ | |
| --title "omp-web $tag" \ | |
| --notes "Updated oh-my-pi to v${OMP_VERSION}." | |
| echo "created=true" >> "$GITHUB_OUTPUT" | |
| - name: Start npm publish for the release tag | |
| if: steps.github_release.outputs.created == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: v${{ steps.metadata.outputs.package_version }} | |
| run: gh workflow run publish-npm.yml --repo "$GITHUB_REPOSITORY" --ref "$RELEASE_TAG" -f ref="$RELEASE_TAG" |