Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .github/workflows/bump-aeo-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Bump aeo-audit

# Keeps the pinned @ainyc/aeo-audit engine current. aeo-audit ships often, so
# this checks npm on a schedule, bumps the exact pin, and — only if the bump
# survives typecheck, lint, build, and the full test suite — opens a PR. The
# build gate matters: aeo-audit is bundled into the published package via tsup,
# so a breaking major can break the bundle without failing typecheck. Gating
# happens IN this job (not just on the PR), so a breaking aeo-audit release
# fails here and never produces a green PR. Run it on demand from the Actions
# tab via "Run workflow".

on:
schedule:
# Mondays 09:00 UTC. Tighten the cron if you ship aeo-audit more often.
- cron: '0 9 * * 1'
workflow_dispatch:
inputs:
version:
description: 'Target @ainyc/aeo-audit version (blank = npm latest dist-tag)'
required: false
default: ''
bump_canonry_version:
description: 'Also patch @ainyc/canonry version (default off: engine updates in-repo, ships with the next canonry release)'
type: boolean
required: false
default: false

permissions:
contents: write
pull-requests: write

# Never let two bump runs race to open competing PRs / branches.
concurrency:
group: bump-aeo-audit
cancel-in-progress: true

jobs:
bump:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10.28.2

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: https://registry.npmjs.org

- name: Resolve target + rewrite package.json pins
id: bump
shell: bash
env:
AEO_AUDIT_VERSION: ${{ github.event.inputs.version }}
# Empty on the schedule, "false" by default on dispatch -> no canonry
# version bump. Toggle the input on (passes "true") to opt in.
BUMP_CANONRY_VERSION: ${{ github.event.inputs.bump_canonry_version }}
run: node scripts/bump-aeo-audit.mjs

- name: Update lockfile + install
if: steps.bump.outputs.changed == 'true'
run: pnpm install --no-frozen-lockfile

- name: Typecheck (gate the bump)
if: steps.bump.outputs.changed == 'true'
run: pnpm run typecheck

- name: Lint (gate the bump)
if: steps.bump.outputs.changed == 'true'
run: pnpm run lint

# Build all packages recursively (matches ci.yml's build job). tsup bundles
# @ainyc/aeo-audit into the published canonry, so this catches a breaking
# major that typecheck misses. Runs before the long test suite to fail fast.
- name: Build (gate the bump)
if: steps.bump.outputs.changed == 'true'
run: pnpm -r run build

- name: Test (gate the bump)
if: steps.bump.outputs.changed == 'true'
run: pnpm run test

- name: Open pull request
if: steps.bump.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v7
with:
# Prefer a configured PAT/app token so the PR triggers the CI workflow;
# fall back to GITHUB_TOKEN (PR still opens, but the default token does
# not re-trigger `ci.yml` — this job's own typecheck/lint/build/test are the gate).
token: ${{ secrets.AEO_AUDIT_BUMP_TOKEN || secrets.GITHUB_TOKEN }}
base: main
branch: chore/bump-aeo-audit
delete-branch: true
commit-message: 'chore(deps): bump @ainyc/aeo-audit to ${{ steps.bump.outputs.to }}'
title: 'chore(deps): bump @ainyc/aeo-audit to ${{ steps.bump.outputs.to }}'
labels: dependencies
body: |
Automated bump of **`@ainyc/aeo-audit`** `${{ steps.bump.outputs.from }}` → `${{ steps.bump.outputs.to }}`.

- ${{ steps.bump.outputs.version_note }}
- This job already ran `pnpm run typecheck`, `pnpm run lint`, `pnpm -r run build`, and `pnpm run test` against the bump — all passed.

Review the aeo-audit changelog for behavioral changes, then merge. Generated by `.github/workflows/bump-aeo-audit.yml`.
2 changes: 1 addition & 1 deletion apps/worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"lint": "eslint src/ test/"
},
"dependencies": {
"@ainyc/aeo-audit": "3.0.0",
"@ainyc/aeo-audit": "4.0.1",
"@ainyc/canonry-config": "workspace:*",
"tsx": "^4.20.5"
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "canonry",
"private": true,
"version": "4.82.0",
"version": "4.83.0",
"type": "module",
"packageManager": "pnpm@10.28.2",
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions packages/canonry/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ainyc/canonry",
"version": "4.82.0",
"version": "4.83.0",
"type": "module",
"description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
"license": "FSL-1.1-ALv2",
Expand Down Expand Up @@ -44,7 +44,7 @@
"lint": "eslint src/ test/"
},
"dependencies": {
"@ainyc/aeo-audit": "3.0.0",
"@ainyc/aeo-audit": "4.0.1",
"@anthropic-ai/sdk": "^0.91.1",
"@fastify/static": "^9.1.1",
"@google/genai": "^1.46.0",
Expand Down
14 changes: 7 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

152 changes: 152 additions & 0 deletions scripts/bump-aeo-audit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env node
// Bump the pinned @ainyc/aeo-audit dependency to a target version.
//
// aeo-audit is the real audit engine (runAeoAudit / runSitemapAudit). canonry
// pins it to an EXACT version on purpose: aeo-audit ships breaking majors
// (e.g. 3.x -> 4.x), and an exact pin forces every bump through CI (typecheck +
// the 4000+ test suite) instead of a floating `^` silently pulling a release
// that changes the report shape mid-build. This script is that controlled bump.
//
// By default it bumps ONLY the engine dependency, not canonry's own version —
// an aeo-audit bump and a canonry npm release are decoupled, so the engine
// updates in-repo and ships with the next canonry release. Pass `--version-bump`
// to also patch canonry so the bump publishes to npm on merge.
//
// Usage (local release step):
// node scripts/bump-aeo-audit.mjs # bump engine to the npm `latest` dist-tag
// node scripts/bump-aeo-audit.mjs 4.1.0 # bump engine to an explicit version
// node scripts/bump-aeo-audit.mjs --version-bump # ALSO patch canonry's version
// pnpm install # then refresh the lockfile + node_modules
//
// Environment (used by .github/workflows/bump-aeo-audit.yml):
// AEO_AUDIT_VERSION target version (overridden by a positional arg)
// BUMP_CANONRY_VERSION "true" to also patch the canonry version (default: no bump)
// GITHUB_OUTPUT when set, the script appends `changed`/`from`/`to`/
// `canonry_from`/`canonry_to`/`version_note` step outputs.
//
// The script only edits files. It never runs `pnpm install`, so the caller
// controls when the lockfile is regenerated.

import { execFileSync } from 'node:child_process'
import { appendFileSync, readFileSync, writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'

const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..')

const DEP = '@ainyc/aeo-audit'
// Every package.json that pins the dependency. Add new consumers here.
const DEP_MANIFESTS = ['packages/canonry/package.json', 'apps/worker/package.json']
// Published packages whose version must stay in lockstep (see AGENTS.md → Versioning).
const VERSION_MANIFESTS = ['package.json', 'packages/canonry/package.json']

function readJson(relPath) {
return JSON.parse(readFileSync(join(repoRoot, relPath), 'utf8'))
}

/**
* Replace a `"key": "value"` string field in a manifest by exact text match so
* the file's existing formatting (indent, key order, trailing newline) is
* preserved — JSON.parse/stringify would risk reflowing the whole file.
*/
function replaceField(relPath, key, expectedValue, nextValue) {
const absPath = join(repoRoot, relPath)
const before = readFileSync(absPath, 'utf8')
const needle = `"${key}": "${expectedValue}"`
if (!before.includes(needle)) {
throw new Error(`Could not find ${needle} in ${relPath} (already bumped, or formatting drifted?)`)
}
writeFileSync(absPath, before.replace(needle, `"${key}": "${nextValue}"`))
}

function resolveLatestVersion() {
// `npm view` reads the npm registry; the `latest` dist-tag is the version a
// bare `npm install @ainyc/aeo-audit` would resolve to.
const out = execFileSync('npm', ['view', DEP, 'dist-tags.latest'], { encoding: 'utf8' })
const version = out.trim()
if (!/^\d+\.\d+\.\d+/.test(version)) {
throw new Error(`Unexpected version from npm for ${DEP}: "${version}"`)
}
return version
}

function bumpPatch(version) {
const match = /^(\d+)\.(\d+)\.(\d+)(.*)$/.exec(version)
if (!match) throw new Error(`Cannot patch-bump non-semver version: "${version}"`)
const [, major, minor, patch] = match
return `${major}.${minor}.${Number(patch) + 1}`
}

function emitOutput(pairs) {
if (!process.env.GITHUB_OUTPUT) return
const lines = Object.entries(pairs).map(([k, v]) => `${k}=${v}`)
appendFileSync(process.env.GITHUB_OUTPUT, `${lines.join('\n')}\n`)
}

function main() {
const args = process.argv.slice(2)
// Canonry's own version is NOT bumped by default — an aeo-audit engine bump and
// a canonry npm release are decoupled. Opt in with `--version-bump` (or
// BUMP_CANONRY_VERSION=true) when you want the bump to ship to npm on merge.
// An explicit `--no-version-bump` still works and wins over any opt-in.
const bumpCanonryVersion =
!args.includes('--no-version-bump') &&
(args.includes('--version-bump') || process.env.BUMP_CANONRY_VERSION === 'true')
const positional = args.find((arg) => !arg.startsWith('--'))
const requested = positional || process.env.AEO_AUDIT_VERSION || ''
const target = requested.trim() || resolveLatestVersion()

// Read the currently-pinned spec from the canonical consumer. Preserve any
// leading range operator (^ / ~) so an intentional range pin stays a range,
// even though canonry pins exact today.
const canonryPkg = readJson('packages/canonry/package.json')
const currentSpec = canonryPkg.dependencies?.[DEP]
if (!currentSpec) throw new Error(`${DEP} not found in packages/canonry/package.json dependencies`)
const rangePrefix = /^[\^~]/.test(currentSpec) ? currentSpec[0] : ''
const currentVersion = currentSpec.replace(/^[\^~]/, '')
const nextSpec = `${rangePrefix}${target}`

if (currentSpec === nextSpec) {
console.log(`${DEP} already at ${currentSpec} — nothing to bump.`)
emitOutput({ changed: 'false', from: currentVersion, to: target })
return
}

for (const manifest of DEP_MANIFESTS) {
const pkg = readJson(manifest)
const spec = pkg.dependencies?.[DEP]
if (!spec) throw new Error(`${DEP} not found in ${manifest} dependencies`)
replaceField(manifest, DEP, spec, nextSpec)
console.log(`${manifest}: ${DEP} ${spec} -> ${nextSpec}`)
}

const canonryFrom = canonryPkg.version
let canonryTo = canonryFrom
if (bumpCanonryVersion) {
canonryTo = bumpPatch(canonryFrom)
for (const manifest of VERSION_MANIFESTS) {
const pkg = readJson(manifest)
replaceField(manifest, 'version', pkg.version, canonryTo)
console.log(`${manifest}: version ${pkg.version} -> ${canonryTo}`)
}
} else {
console.log(`Leaving canonry version at ${canonryFrom} (default; pass --version-bump to ship on merge).`)
}

const versionNote = bumpCanonryVersion
? `\`@ainyc/canonry\` ${canonryFrom} -> ${canonryTo} (ships to npm on merge).`
: '`@ainyc/canonry` version unchanged — the engine updates in-repo and ships with the next canonry release.'

emitOutput({
changed: 'true',
from: currentVersion,
to: target,
canonry_from: canonryFrom,
canonry_to: canonryTo,
version_note: versionNote,
})

console.log(`\nBumped ${DEP} ${currentVersion} -> ${target}. Next: run \`pnpm install\` to update the lockfile.`)
}

main()
Loading