Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
05ef755
Set up project tooling and build config
aswasif007 Jul 20, 2026
4d29a5d
Add the integration conformance validator
aswasif007 Jul 20, 2026
2557d17
Add the Starter Kit init scaffolder
aswasif007 Jul 20, 2026
a3f07d3
Wire up the a8c-integration CLI entry point
aswasif007 Jul 20, 2026
b83e1d4
Add CI workflows and npm release tooling
aswasif007 Jul 20, 2026
402b034
Add README and documentation
aswasif007 Jul 20, 2026
5466067
Use "Wordpress" instead of "Acme" as example vendor name
aswasif007 Jul 20, 2026
a59ebd9
Frame init output as a fresh scaffold, not a rewrite
aswasif007 Jul 20, 2026
df3d7a8
Remove the built-in help subcommand
aswasif007 Jul 20, 2026
b3c61b7
Add vip dev-env step to init next steps
aswasif007 Jul 20, 2026
d60d4db
Remove ... from desc
aswasif007 Jul 20, 2026
0c88b23
Tighten Rule 2 and Rule 8 conformance checks
aswasif007 Jul 20, 2026
668f8e2
Skip symlinks in the scaffolder file walk
aswasif007 Jul 20, 2026
dba1bea
Reject no-op validate-integration script in Rule 3
aswasif007 Jul 20, 2026
9dc0504
Fix realCommands dropping real commands chained after a banner
aswasif007 Jul 21, 2026
22a14e7
Validate the handoff manifest instead of a composer script
aswasif007 Jul 24, 2026
5d44368
Clean up a half-scaffolded directory on init failure
aswasif007 Jul 24, 2026
58c0288
Skip binary files by NUL sniff in the scaffolder
aswasif007 Jul 24, 2026
b2962cc
Tag the built tree in the npm release workflow
aswasif007 Jul 24, 2026
7b3dda0
Use "WordPress" in example vendor names
aswasif007 Jul 24, 2026
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
46 changes: 46 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Static analysis

on:
pull_request:
push:
branches:
- trunk

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
lint:
name: eslint, prettier, types, build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up pnpm
uses: pnpm/action-setup@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts

- name: Run ESLint
run: pnpm lint

- name: Check formatting
run: pnpm format:check

- name: Check types
run: pnpm check-types

- name: Build
run: pnpm build
106 changes: 106 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Publish npm package

on:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-trunk
cancel-in-progress: false

permissions:
contents: write
id-token: write

jobs:
publish:
name: publish
runs-on: ubuntu-latest
environment: npm-publish
steps:
- name: Checkout trunk
uses: actions/checkout@v4
with:
ref: trunk
fetch-depth: 1

- name: Set up pnpm
uses: pnpm/action-setup@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm

- name: Ensure npm supports trusted publishing
run: npm install -g npm@11.5.1

- name: Read package metadata
id: package
run: |
node <<'NODE'
const fs = require( 'node:fs' );
const pkg = JSON.parse( fs.readFileSync( 'package.json', 'utf8' ) );
fs.appendFileSync( process.env.GITHUB_OUTPUT, `name=${ pkg.name }\n` );
fs.appendFileSync( process.env.GITHUB_OUTPUT, `version=${ pkg.version }\n` );
NODE

- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts

- name: Run ESLint
run: pnpm lint

- name: Check formatting
run: pnpm format:check

- name: Check types
run: pnpm check-types

- name: Run tests
run: pnpm test

- name: Build package
run: pnpm build

- name: Check GitHub release does not exist
env:
GH_TOKEN: ${{ github.token }}
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
run: |
set -euo pipefail
tag_name="v$PACKAGE_VERSION"
if gh release view "$tag_name" >/dev/null 2>&1; then
echo "::error::GitHub release $tag_name already exists."
exit 1
fi

- name: Check npm version is unpublished
env:
PACKAGE_NAME: ${{ steps.package.outputs.name }}
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
run: |
set -euo pipefail
if npm view "$PACKAGE_NAME@$PACKAGE_VERSION" version >/dev/null 2>&1; then
echo "::error::$PACKAGE_NAME@$PACKAGE_VERSION is already published on npm."
exit 1
fi

- name: Preview npm package
run: npm pack --dry-run

- name: Publish to npm
run: npm publish --access public

- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
run: |
set -euo pipefail
tag_name="v$PACKAGE_VERSION"
# Tag the tree we actually built and published — this job checks out and
# builds `trunk`, so tag trunk's tip. Using $GITHUB_SHA would tag the
# commit the workflow was dispatched from (a feature branch on a manual
# run), pointing the release at code that was never shipped.
gh release create "$tag_name" --target "$(git rev-parse HEAD)" --title "$tag_name" --generate-notes
40 changes: 40 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Test

on:
pull_request:
push:
branches:
- trunk

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
test:
name: jest
runs-on: ubuntu-latest
strategy:
matrix:
node: ['20', '22']
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up pnpm
uses: pnpm/action-setup@v4

- name: Set up Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts

- name: Run tests
run: pnpm test
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
coverage/
*.log
.DS_Store
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
coverage/
pnpm-lock.yaml
bin/a8c-integration
1 change: 1 addition & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"@automattic/eslint-plugin-wpvip/prettierrc"
71 changes: 69 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,70 @@
# integration
# a8c-integration

WIP
A CLI for building **WordPress VIP Integration Center** add-ons. It does two things:

- **`a8c-integration init`** — scaffold a new integration from the [VIP Integrations Starter Kit](https://github.com/Automattic/vip-integrations-starter-kit), with the example prefix set already rewritten to your names.
- **`a8c-integration validate`** — run the integration conformance checker locally and in CI, so you get an objective _conformant / not-conformant_ answer before you submit.

It is a standalone home for the checker that used to live in `vip-cli`, decoupled so the Integration Center can extend to other parts of Automattic (.com / a4a).

## Install

```bash
npm install -g @automattic/a8c-integration
```

Or run without installing:

```bash
npx @automattic/a8c-integration validate
```

Requires Node.js 20+.

## Usage

### Start a new integration

```bash
a8c-integration init
```

Interactive — it asks for your **vendor name** and **integration name**, always builds from the canonical [VIP Integrations Starter Kit](https://github.com/Automattic/vip-integrations-starter-kit) (its default branch, no git history pulled), rewrites the example prefix set to your names, renames the entry file, and starts a fresh git history. You can also pass the answers as flags:

```bash
a8c-integration init --vendor "WordPress" --name "Content Sync"
```

| Flag | Description |
| ------------------- | ---------------------------------------------------- |
| `--vendor <vendor>` | Vendor name, e.g. `"My Vendor"`. |
| `--name <name>` | Integration name, e.g. `"Content Sync"`. |
| `--dir <dir>` | Target directory (defaults to the integration slug). |

The Starter Kit source is not configurable — `init` always uses the official VIP repo.

When it finishes:

```bash
cd content-sync
composer install && npm install
# edit your integration, then:
a8c-integration validate
```

### Validate an integration

```bash
a8c-integration validate # checks the current directory
a8c-integration validate ./my-plugin # checks a given directory
a8c-integration validate --format json # machine-readable output for CI
```

The checker runs nine static conformance rules and prints a per-rule report. It exits `1` when the integration is **not conformant** (any rule failed), so it gates CI. Warnings do not break conformance. Two items — the plugin/platform config-schema match and the security review — are surfaced as _human review required_ rather than automated pass/fail, because they cannot be checked statically.

## Documentation

- [Setup](docs/setup.md) — install, build, and run from source.
- [Architecture](docs/architecture.md) — how the CLI and the checker are put together.
- [Testing](docs/testing.md) — how to run and extend the test suite.
- [Releasing](docs/releasing.md) — how to cut and publish an npm release.
60 changes: 60 additions & 0 deletions __tests__/report.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { formatHumanReport, formatJsonReport } from '../src/lib/validate/report';

import type { ValidationReport } from '../src/lib/validate/validate';

function sampleReport( conformant: boolean ): ValidationReport {
return {
path: '/tmp/acme',
results: [
{ id: 'a', rule: 1, title: 'First rule', status: 'pass', message: 'looks good' },
{
id: 'b',
rule: 2,
title: 'Second rule',
status: conformant ? 'warn' : 'fail',
message: conformant ? 'heads up' : 'broken',
details: [ 'evidence line' ],
},
{ id: 'c', rule: 3, title: 'Third rule', status: 'not_applicable', message: 'skipped' },
],
humanReview: [ { title: 'Security review', reason: 'human only' } ],
conformant,
configChecksSkipped: false,
};
}

describe( 'validate report', () => {
it( 'renders a human report with per-rule verdicts and the human-review section', () => {
const text = formatHumanReport( sampleReport( true ) );
expect( text ).toContain( 'Rule 1: First rule' );
expect( text ).toContain( 'evidence line' );
expect( text ).toContain( 'Human review required' );
expect( text ).toContain( 'Security review' );
expect( text ).toContain( 'Conformant' );
expect( text ).toContain( '1 passed' );
} );

it( 'marks a non-conformant report clearly', () => {
const text = formatHumanReport( sampleReport( false ) );
expect( text ).toContain( 'Not conformant' );
expect( text ).toContain( '1 failed' );
} );

it( 'warns loudly when config checks were skipped', () => {
const text = formatHumanReport( { ...sampleReport( true ), configChecksSkipped: true } );
expect( text ).toContain( 'Config checks (rules 4-6) did NOT run' );
} );

it( 'omits the config-skipped warning when config was checked', () => {
const text = formatHumanReport( sampleReport( true ) );
expect( text ).not.toContain( 'did NOT run' );
} );

it( 'emits valid, round-trippable JSON', () => {
const report = sampleReport( false );
const parsed = JSON.parse( formatJsonReport( report ) ) as ValidationReport;
expect( parsed.conformant ).toBe( false );
expect( parsed.results ).toHaveLength( 3 );
expect( parsed.humanReview[ 0 ].title ).toBe( 'Security review' );
} );
} );
Loading
Loading