Skip to content

Commit 2e4316d

Browse files
committed
feat: add GitHub Actions publish workflow and update package repository metadata
1 parent 555e0a8 commit 2e4316d

5 files changed

Lines changed: 213 additions & 1 deletion

File tree

‎.github/workflows/publish.yml‎

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
name: Publish
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
contents: read
9+
10+
concurrency:
11+
group: npm-publish
12+
cancel-in-progress: false
13+
14+
jobs:
15+
publish:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 45
18+
permissions:
19+
contents: read
20+
id-token: write
21+
steps:
22+
- uses: actions/checkout@v7
23+
with:
24+
ref: ${{ github.event.release.tag_name }}
25+
persist-credentials: false
26+
- uses: actions/setup-node@v7
27+
with:
28+
node-version-file: .node-version
29+
registry-url: https://registry.npmjs.org
30+
package-manager-cache: false
31+
- uses: pnpm/setup@v3
32+
with:
33+
version: 12.3.4
34+
- name: Install OIDC-capable npm
35+
run: npm install --global npm@11.21.0 --ignore-scripts
36+
- run: pnpm install --frozen-lockfile
37+
- name: Validate release version and channel
38+
id: release
39+
env:
40+
RELEASE_TAG: ${{ github.event.release.tag_name }}
41+
RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
42+
run: |
43+
node --input-type=module <<'NODE'
44+
import { appendFileSync } from 'node:fs';
45+
import semver from 'semver';
46+
47+
const version = semver.valid(process.env.RELEASE_TAG);
48+
if (!version || process.env.RELEASE_TAG.includes('+')) {
49+
throw new Error('Release tag must be SemVer without build metadata, optionally prefixed with v');
50+
}
51+
const prerelease = semver.prerelease(version) !== null;
52+
if (String(prerelease) !== process.env.RELEASE_PRERELEASE) {
53+
throw new Error('Release prerelease flag must match the SemVer tag');
54+
}
55+
appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\ntag=${prerelease ? 'next' : 'latest'}\n`);
56+
NODE
57+
- run: pnpm run check
58+
- name: Require deterministic generated files
59+
run: git diff --exit-code
60+
- name: Set plugin release version
61+
uses: TypescriptPrime/bump-packagejson-version@8b461d950090eae85d58d8a0085bfc993abdea0d # 1.1.9
62+
with:
63+
package-json-dir-path: packages/oxlint-plugin
64+
- name: Set catalog release version
65+
uses: TypescriptPrime/bump-packagejson-version@8b461d950090eae85d58d8a0085bfc993abdea0d # 1.1.9
66+
with:
67+
package-json-dir-path: packages/rule-catalog
68+
- name: Set config release version
69+
uses: TypescriptPrime/bump-packagejson-version@8b461d950090eae85d58d8a0085bfc993abdea0d # 1.1.9
70+
with:
71+
package-json-dir-path: packages/oxlint-config
72+
- name: Build and test release packages
73+
run: |
74+
pnpm run build
75+
pnpm exec vitest run tests/consumer.test.ts
76+
- name: Pack and validate all release artifacts
77+
id: pack
78+
env:
79+
RELEASE_VERSION: ${{ steps.release.outputs.version }}
80+
DIST_TAG: ${{ steps.release.outputs.tag }}
81+
run: |
82+
node --input-type=module <<'NODE'
83+
import assert from 'node:assert/strict';
84+
import { execFileSync } from 'node:child_process';
85+
import { appendFileSync, mkdirSync, mkdtempSync, readdirSync } from 'node:fs';
86+
import { join, resolve } from 'node:path';
87+
88+
const directory = mkdtempSync(join(process.env.RUNNER_TEMP, 'ag-publish-'));
89+
const version = process.env.RELEASE_VERSION;
90+
for (const name of ['oxlint-plugin', 'rule-catalog', 'oxlint-config']) {
91+
const destination = join(directory, name);
92+
mkdirSync(destination);
93+
execFileSync('pnpm', ['pack', '--pack-destination', destination], {
94+
cwd: resolve('packages', name), stdio: 'inherit',
95+
});
96+
const archives = readdirSync(destination).filter((file) => file.endsWith('.tgz'));
97+
assert.equal(archives.length, 1, `Expected one archive for ${name}`);
98+
const archive = join(destination, archives[0]);
99+
const manifest = JSON.parse(execFileSync('tar', ['-xOf', archive, 'package/package.json'], {
100+
encoding: 'utf8',
101+
}));
102+
assert.equal(manifest.name, `@agcodeguidelines/${name}`);
103+
assert.equal(manifest.version, version);
104+
assert.notEqual(manifest.private, true);
105+
assert.equal(manifest.repository.type, 'git');
106+
assert.equal(manifest.repository.url, `git+https://github.com/${process.env.GITHUB_REPOSITORY}.git`);
107+
assert.equal(manifest.repository.directory, `packages/${name}`);
108+
for (const section of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
109+
for (const range of Object.values(manifest[section] ?? {})) {
110+
assert.ok(!range.startsWith('workspace:'), `Unresolved workspace dependency in ${name}`);
111+
}
112+
}
113+
if (name === 'oxlint-config') {
114+
assert.equal(manifest.dependencies['@agcodeguidelines/oxlint-plugin'], version);
115+
}
116+
const files = execFileSync('tar', ['-tzf', archive], { encoding: 'utf8' }).split('\n');
117+
for (const file of ['dist/index.js', 'dist/index.d.ts', 'LICENSE', 'README.md', 'THIRD_PARTY_NOTICES.md']) {
118+
assert.ok(files.includes(`package/${file}`), `Missing ${file} in ${name}`);
119+
}
120+
if (name === 'oxlint-config') {
121+
assert.ok(files.includes('package/tsconfig.base.json'));
122+
}
123+
execFileSync('npm', ['publish', archive, '--dry-run', '--access', 'public', '--tag', process.env.DIST_TAG], {
124+
stdio: 'inherit',
125+
});
126+
}
127+
appendFileSync(process.env.GITHUB_OUTPUT, `directory=${directory}\n`);
128+
NODE
129+
- name: Reject already published versions
130+
env:
131+
RELEASE_VERSION: ${{ steps.release.outputs.version }}
132+
run: |
133+
node --input-type=module <<'NODE'
134+
import { spawnSync } from 'node:child_process';
135+
136+
for (const name of ['oxlint-plugin', 'rule-catalog', 'oxlint-config']) {
137+
const result = spawnSync('npm', ['view', `@agcodeguidelines/${name}`, 'versions', '--json',
138+
'--registry=https://registry.npmjs.org'], { encoding: 'utf8' });
139+
if (result.error) throw result.error;
140+
if (result.status !== 0) {
141+
const failure = JSON.parse(result.stdout || '{}');
142+
if (result.status !== null && failure.error?.code === 'E404') continue;
143+
throw new Error(`Cannot check ${name}: ${result.stderr}`);
144+
}
145+
const versions = JSON.parse(result.stdout);
146+
const published = Array.isArray(versions) ? versions : [versions];
147+
if (published.includes(process.env.RELEASE_VERSION)) {
148+
throw new Error(`${name}@${process.env.RELEASE_VERSION} already exists; inspect any partial release before retrying`);
149+
}
150+
}
151+
NODE
152+
- name: Publish with npm trusted publishing
153+
env:
154+
PACK_DIRECTORY: ${{ steps.pack.outputs.directory }}
155+
DIST_TAG: ${{ steps.release.outputs.tag }}
156+
run: |
157+
for package in oxlint-plugin rule-catalog oxlint-config; do
158+
archives=( "$PACK_DIRECTORY/$package/"*.tgz )
159+
[[ ${#archives[@]} -eq 1 && -f "${archives[0]}" ]]
160+
npm publish "${archives[0]}" --access public --tag "$DIST_TAG"
161+
done

‎docs/maintenance.md‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ The weekly GitHub workflow opens a **signed draft PR**, runs acceptance checks,
2727

2828
Review the generated rule catalog, effective sample options and disabled rules, fixture changes, exclusions, source/patch hashes, license notices, and all platform checks. Use `pnpm run snapshots:record` only after intentionally reviewing a snapshot change; normal checks reject drift.
2929

30-
Local commits use `git commit -S` and include `Co-authored-by: Codex <codex@openai.com>`. Published packages are prepared with `pnpm pack`; publication is a separate maintainer action.
30+
Local commits use `git commit -S` and include `Co-authored-by: Codex <codex@openai.com>`. Published packages are prepared with `pnpm pack`; maintainers initiate publication by publishing a GitHub Release as described below.
3131

3232
All 540 JavaScript sources from the migration baseline now have TypeScript ports: 308 core files, 44 JSDoc files, 133 React files, 54 import files, and the reference configuration. Emitted JavaScript in package `dist` directories remains the consumer runtime format. Upstream JavaScript filenames in provenance, patch inputs, and fixture strings remain unchanged.
3333

@@ -37,6 +37,42 @@ All 540 JavaScript sources from the migration baseline now have TypeScript ports
3737

3838
The existing development overrides permit sequential `for...of` traversal and early `continue` in the scope, context, and dependency parser adapters, where mutable graph traversal is required. Parser nodes are annotated in place. Internal application helpers retain named exports where already configured. All vendor families follow the repository's TypeScript import extension policy and full lint rules. Test-capture classes model upstream APIs. These development settings do not change the published presets.
3939

40+
## npm releases
41+
42+
`.github/workflows/publish.yml` runs when a GitHub Release is published. It checks out the release tag, installs the frozen dependencies, runs `pnpm run check`, and rejects generated-file drift before preparing any release artifacts. Review the normal cross-platform Check workflow before publishing a release; the Publish workflow independently runs checks on Ubuntu, not the full platform matrix.
43+
44+
Use a SemVer tag such as `v1.2.3` for a regular release or `v1.2.3-rc.1` for a prerelease. The GitHub Release's prerelease flag must match the tag. Build metadata (`+...`) is rejected to avoid npm version normalization collisions. All three public packages receive the same version through `TypescriptPrime/bump-packagejson-version`, pinned to the reviewed `1.1.9` commit. The action installs its own dependencies at runtime, so the commit pin does not freeze that dependency resolution. Version changes are made only in the runner: the private root version, committed package versions, and dependency lockfile remain unchanged, and the workflow never commits or pushes.
45+
46+
After updating versions, the workflow rebuilds and reruns the packed-consumer tests. It prepares all packages with `pnpm pack`, validates the archive manifests and required files, and performs `npm publish --dry-run`. Packing converts the config's `workspace:*` plugin dependency to the exact release version. Publication uses the npm CLI, not `pnpm publish`, in this order:
47+
48+
1. `@agcodeguidelines/oxlint-plugin`
49+
2. `@agcodeguidelines/rule-catalog`
50+
3. `@agcodeguidelines/oxlint-config`
51+
52+
Regular releases use `latest`; prereleases use `next`. Only these public packages are published. A shared concurrency group prevents simultaneous publication, and an active run is never canceled by a newer release. GitHub concurrency retains only one pending run; publish releases one at a time and confirm each completes before starting the next.
53+
54+
### Trusted publisher setup
55+
56+
For each package, configure a GitHub Actions trusted publisher in its npm settings:
57+
58+
- Organization or user: `FilteringDev`
59+
- Repository: `AGCodeGuidelines-TS`
60+
- Workflow filename: `publish.yml` (not the full path)
61+
- Environment name: leave empty; this workflow does not use a GitHub environment
62+
- Allow direct `npm publish`; stage-only permission is not sufficient
63+
64+
The workflow uses a GitHub-hosted runner, `id-token: write`, Node from `.node-version`, and npm `11.21.0` (trusted publishing requires npm >=11.5.1 and Node >=22.14.0). It does not need an `NPM_TOKEN` or `NODE_AUTH_TOKEN` secret. The public package manifests identify this repository and their workspace directories. npm automatically attaches provenance to OIDC publications from this public repository; no separate provenance token is needed.
65+
66+
A new trusted publisher configuration must complete its first successful publish within two days; otherwise delete and recreate it. If a package does not yet exist and its trusted publisher cannot be configured, an authorized maintainer must bootstrap it manually with `pnpm pack` and an authenticated npm CLI before registering the trusted publisher. Use a new release version afterward. Do not add a long-lived token fallback to the workflow. After confirming OIDC publication works, npm recommends requiring two-factor authentication and disallowing traditional publishing tokens.
67+
68+
See [npm's trusted publishing documentation](https://docs.npmjs.com/trusted-publishers/) for setup and troubleshooting. Dry runs and `npm whoami` do not validate OIDC publishing permission; the first real release is the authentication and provenance acceptance check.
69+
70+
### Failed or partial releases
71+
72+
The workflow checks all three packages for an existing release version before the first publish. An existing version stops the run; registry/authentication/network failures other than a package-not-found response also stop the run. npm versions cannot be overwritten, and three publish operations are not atomic.
73+
74+
If a run fails, inspect the workflow log and the registry versions and dist-tags for all three packages before retrying. If none were published, rerun the failed workflow after fixing the cause. If any were published, do not blindly rerun: the duplicate-version guard will stop it. Prefer fixing the cause and publishing a new version for all three packages. If completing the original version is necessary, an authorized maintainer must reproduce the tagged build and version updates, validate the packed artifacts, and manually publish only the missing packages with the original `latest` or `next` tag. Never republish an existing version or move dist-tags without review.
75+
4076
## Read-only consumer rehearsals
4177

4278
External projects under `/references/outside` are reference evidence, not working directories. Never install, build, lint, fix, or run tests there. On Linux or macOS with `rsync` installed, prepare independent copies with:

‎packages/oxlint-config/package.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{
22
"name": "@agcodeguidelines/oxlint-config",
33
"version": "0.1.0",
4+
"repository": {
5+
"type": "git",
6+
"url": "git+https://github.com/FilteringDev/AGCodeGuidelines-TS.git",
7+
"directory": "packages/oxlint-config"
8+
},
49
"description": "Reproducible AdGuard JavaScript and TypeScript guidelines for Oxlint",
510
"type": "module",
611
"exports": {

‎packages/oxlint-plugin/package.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{
22
"name": "@agcodeguidelines/oxlint-plugin",
33
"version": "0.1.0",
4+
"repository": {
5+
"type": "git",
6+
"url": "git+https://github.com/FilteringDev/AGCodeGuidelines-TS.git",
7+
"directory": "packages/oxlint-plugin"
8+
},
49
"description": "AdGuard guideline rules and JavaScript rule adapters for Oxlint",
510
"type": "module",
611
"exports": {

‎packages/rule-catalog/package.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{
22
"name": "@agcodeguidelines/rule-catalog",
33
"version": "0.1.0",
4+
"repository": {
5+
"type": "git",
6+
"url": "git+https://github.com/FilteringDev/AGCodeGuidelines-TS.git",
7+
"directory": "packages/rule-catalog"
8+
},
49
"type": "module",
510
"exports": {
611
".": {

0 commit comments

Comments
 (0)