release-agent-skills 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/skills/release-maven/SKILL.md +8 -3
- package/skills/release-maven/errors.md +2 -1
- package/skills/release-maven/reference/signing-setup.md +2 -0
- package/skills/release-maven/scripts/check-credentials.mjs +17 -3
- package/skills/release-maven/scripts/inspect-artifacts.mjs +7 -7
- package/skills/release-maven/scripts/skip-signing.init.gradle.kts +8 -0
- package/skills/release-maven/scripts/verify-signatures.mjs +8 -7
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.1] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `skip-signing.init.gradle.kts` and an `--unsigned-expected` flag for `inspect-artifacts.mjs` and `verify-signatures.mjs`, so check-only runs work on machines without signing keys when the keys live in CI. Missing signatures are then reported as warnings while POMs, sources, javadoc and KMP targets are still checked.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `check-credentials.mjs` reports whether gpg is installed and, without gpg, recommends the in-memory key setup.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Check-only mode no longer writes `build/release-agent/state.json`, so a later run does not mistake a check for a release in progress.
|
|
23
|
+
|
|
10
24
|
## [0.1.0] - 2026-10-28
|
|
11
25
|
|
|
12
26
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "release-agent-skills",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "AI agent skills that publish libraries safely. v0.1: Maven Central through the Sonatype Central Portal, with pre-flight checks, approval gates and manual release by default.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -59,7 +59,7 @@ Settle two decisions before going on. Both appear as `warn` checks when they app
|
|
|
59
59
|
1. **Release path** (`detect.releaseAutomation`). The project already publishes from CI. A local release would skip whatever else that workflow does (generated files, release assets, tags). Recommend `ci`: run phases 2 to 4 locally as checks, then trigger the workflow in phase 6. Use `local` only if the user chooses it.
|
|
60
60
|
2. **Version strategy** (`detect.versionStrategy`). The committed version is a SNAPSHOT held in a Gradle property. Recommend `override`: leave the file alone and pass `-P<versionProperty>=<new>` to every Gradle command. The alternative is `file`: change the file in phase 3. With `override`, set `VERSION_ARG="-P<versionProperty>=<new>"`; otherwise `VERSION_ARG=""`.
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
In `manual` or `automatic` mode, save both: `state.mjs set releasePath=<ci|local> versionStrategy=<override|file> version=<new>`. In `check-only` mode, write no state at all, so that a later run does not mistake the check for a release in progress.
|
|
63
63
|
|
|
64
64
|
## Phase 2: Static pre-flight (no build)
|
|
65
65
|
|
|
@@ -98,7 +98,7 @@ node "$SKILL_DIR/../release-agent-shared/scripts/changelog.mjs" --version <new>
|
|
|
98
98
|
./gradlew build $VERSION_ARG
|
|
99
99
|
REPO="$PWD/build/release-agent/repo-$(date +%Y%m%d-%H%M%S)"
|
|
100
100
|
./gradlew publishToMavenLocal $VERSION_ARG -Dmaven.repo.local="$REPO" --no-configuration-cache
|
|
101
|
-
node "$SKILL_DIR/../release-agent-shared/scripts/state.mjs" set stagingRepo="$REPO"
|
|
101
|
+
node "$SKILL_DIR/../release-agent-shared/scripts/state.mjs" set stagingRepo="$REPO" # not in check-only
|
|
102
102
|
node "$SKILL_DIR/scripts/inspect-artifacts.mjs" --repo "$REPO" --group <g> --version <v> --project .
|
|
103
103
|
node "$SKILL_DIR/scripts/validate-pom.mjs" --repo "$REPO" --version <v>
|
|
104
104
|
node "$SKILL_DIR/scripts/verify-signatures.mjs" --repo "$REPO" --version <v>
|
|
@@ -109,7 +109,12 @@ node "$SKILL_DIR/scripts/verify-signatures.mjs" --repo "$REPO" --version <v>
|
|
|
109
109
|
- If the project already runs a narrower verification in CI (see the release workflow), prefer those tasks over `build`, which can be slow for KMP.
|
|
110
110
|
- `inspect-artifacts.mjs` checks for a POM, main artifact, `-sources.jar`, `-javadoc.jar` and a `.asc` for each file. It also checks that every artifact expected from the build files is staged, and that every target referenced by a KMP root `.module` file is present.
|
|
111
111
|
- `verify-signatures.mjs` checks every `.asc`, verifies with `gpg` when installed, and checks that the public key is on a supported keyserver.
|
|
112
|
-
-
|
|
112
|
+
- **Keys only in CI.** When `check-credentials.mjs` found no signing setup and the release path is `ci` (or the mode is `check-only`), `publishToMavenLocal` fails with "no configured signatory" (errors.md E11). Stage without signing instead, and tell the user the staged files are unsigned on purpose:
|
|
113
|
+
```sh
|
|
114
|
+
./gradlew publishToMavenLocal $VERSION_ARG -Dmaven.repo.local="$REPO" --no-configuration-cache \
|
|
115
|
+
--init-script "$SKILL_DIR/scripts/skip-signing.init.gradle.kts"
|
|
116
|
+
```
|
|
117
|
+
Then pass `--unsigned-expected` to `inspect-artifacts.mjs` and `verify-signatures.mjs`, so missing `.asc` files are reported as `warn`. POMs, sources, javadoc and KMP targets are still fully checked. Never use the init script or the flag with release path `local` or with any upload command.
|
|
113
118
|
- In `check-only` mode, stop here and print a go/no-go table: one row per check, with the fix for every failure.
|
|
114
119
|
|
|
115
120
|
## Phase 5: Gate 1 (before upload)
|
|
@@ -117,7 +117,8 @@ Each entry gives the cause, the fix, and the phase to rerun from. Propose the fi
|
|
|
117
117
|
|
|
118
118
|
- **Match:** `Cannot perform signing task ... because it has no configured signatory`
|
|
119
119
|
- **Cause:** Signing is required, but Gradle found none of the signing properties.
|
|
120
|
-
- **Fix:** Configure one of the three setups in `reference/signing-setup.md`. Check the property names with `check-credentials.mjs`.
|
|
120
|
+
- **Fix:** Configure one of the three setups in `reference/signing-setup.md`. Check the property names with `check-credentials.mjs`. If it reports `gpgInstalled: false`, use the in-memory key: it is the only setup that works without `gpg`.
|
|
121
|
+
- **Check-only with keys only in CI:** this error is expected on a machine without keys. Stage with `--init-script "$SKILL_DIR/scripts/skip-signing.init.gradle.kts"` and pass `--unsigned-expected` to the checks (SKILL.md, Phase 4). With environment variables, confirm they are exported in the shell that runs Gradle and that the Gradle daemon was started after they were set (`./gradlew --stop` restarts it).
|
|
121
122
|
- **Rerun from:** Phase 4
|
|
122
123
|
|
|
123
124
|
## E12 Cannot read the signing key
|
|
@@ -38,6 +38,8 @@ Pick one. `check-credentials.mjs` reports which one it found, by property name o
|
|
|
38
38
|
|
|
39
39
|
### 1. In-memory key (recommended, works for local and CI)
|
|
40
40
|
|
|
41
|
+
This is the only setup that needs no `gpg` on the machine that builds. Use it when `check-credentials.mjs` reports `gpgInstalled: false`. Exporting the key once requires `gpg` on some machine (or an existing CI secret holding the armored key).
|
|
42
|
+
|
|
41
43
|
| Gradle property | Environment variable | Notes |
|
|
42
44
|
|---|---|---|
|
|
43
45
|
| `signingInMemoryKey` | `ORG_GRADLE_PROJECT_signingInMemoryKey` | ASCII-armored private key |
|
|
@@ -18,6 +18,15 @@ function usesGpgCmd(projectDir) {
|
|
|
18
18
|
return dirs.some((d) => files.some((f) => existsSync(join(d, f)) && /useGpgCmd\s*\(/.test(readFileSync(join(d, f), 'utf8'))));
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function gpgInstalled() {
|
|
22
|
+
try {
|
|
23
|
+
execFileSync('gpg', ['--version'], { stdio: 'ignore' });
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
function gpgSecretKeyCount() {
|
|
22
31
|
try {
|
|
23
32
|
const out = execFileSync('gpg', ['--batch', '--list-secret-keys', '--with-colons'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
@@ -27,7 +36,7 @@ function gpgSecretKeyCount() {
|
|
|
27
36
|
}
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
export function checkCredentials({ setup = 'any', projectDir = '.', env = process.env, home, gpgCount = gpgSecretKeyCount } = {}) {
|
|
39
|
+
export function checkCredentials({ setup = 'any', projectDir = '.', env = process.env, home, gpgCount = gpgSecretKeyCount, hasGpg = gpgInstalled } = {}) {
|
|
31
40
|
if (!['vanniktech', 'maven-publish', 'any'].includes(setup)) throw new UsageError(`Unknown setup "${setup}"`);
|
|
32
41
|
const sources = credentialSources({ projectDir, env, home });
|
|
33
42
|
const checks = [];
|
|
@@ -68,9 +77,14 @@ export function checkCredentials({ setup = 'any', projectDir = '.', env = proces
|
|
|
68
77
|
if (count) signing.push(`gpg agent (useGpgCmd, ${count} secret key(s) in the local keyring)`);
|
|
69
78
|
else checks.push(check('credentials.gpg', 'fail', 'The build uses useGpgCmd() but gpg has no secret keys or is not installed', { fix: 'See reference/signing-setup.md.' }));
|
|
70
79
|
}
|
|
80
|
+
const gpg = hasGpg();
|
|
71
81
|
checks.push(signing.length
|
|
72
82
|
? check('credentials.signing', 'pass', `Signing configured: ${signing.join('; ')}`)
|
|
73
|
-
: check('credentials.signing', 'fail',
|
|
83
|
+
: check('credentials.signing', 'fail', `No signing configuration found${gpg ? '' : ' (gpg is not installed)'}`, {
|
|
84
|
+
fix: gpg
|
|
85
|
+
? 'Configure one of the setups in reference/signing-setup.md (errors.md E11).'
|
|
86
|
+
: 'Use the in-memory key setup (signingInMemoryKey, signingInMemoryKeyPassword); it needs no gpg on this machine. See reference/signing-setup.md (errors.md E11).',
|
|
87
|
+
}));
|
|
74
88
|
|
|
75
89
|
// Secrets must not sit in files that are committed.
|
|
76
90
|
for (const key of SECRET_KEYS) {
|
|
@@ -84,7 +98,7 @@ export function checkCredentials({ setup = 'any', projectDir = '.', env = proces
|
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
return result(checks, { keys: Object.values(report).map(({ key, found, source }) => ({ key, found, source })) });
|
|
101
|
+
return result(checks, { gpgInstalled: gpg, keys: Object.values(report).map(({ key, found, source }) => ({ key, found, source })) });
|
|
88
102
|
}
|
|
89
103
|
|
|
90
104
|
await runCli(import.meta.url, {
|
|
@@ -15,7 +15,7 @@ function moduleTargets(dir, artifactId, version) {
|
|
|
15
15
|
return [...new Set((meta.variants ?? []).map((v) => v['available-at']?.module).filter(Boolean))];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function inspectArtifacts({ repo, group, version, expected = [] }) {
|
|
18
|
+
export function inspectArtifacts({ repo, group, version, expected = [], unsignedExpected = false }) {
|
|
19
19
|
const checks = [];
|
|
20
20
|
const components = listComponents(repo, group, version);
|
|
21
21
|
if (components.length === 0) {
|
|
@@ -52,9 +52,9 @@ export function inspectArtifacts({ repo, group, version, expected = [] }) {
|
|
|
52
52
|
} else {
|
|
53
53
|
checks.push(check(`artifacts.${artifactId}.files`, 'pass', `${artifactId}: POM, main artifact, sources and javadoc present`));
|
|
54
54
|
}
|
|
55
|
-
checks.push(
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
if (!unsigned.length) checks.push(check(`artifacts.${artifactId}.signatures`, 'pass', `${artifactId}: every file has a .asc signature`));
|
|
56
|
+
else if (unsignedExpected) checks.push(check(`artifacts.${artifactId}.signatures`, 'warn', `${artifactId}: ${unsigned.length} file(s) unsigned, as expected: signing runs in CI`));
|
|
57
|
+
else checks.push(check(`artifacts.${artifactId}.signatures`, 'fail', `${artifactId}: ${unsigned.length} file(s) without .asc: ${unsigned.join(', ')}`, { fix: 'Signing did not run for these files (errors.md E06).' }));
|
|
58
58
|
|
|
59
59
|
const targets = moduleTargets(dir, artifactId, version);
|
|
60
60
|
const missingTargets = targets.filter((t) => !present.has(t));
|
|
@@ -83,10 +83,10 @@ function expectedFromProject(projectDir, group) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
await runCli(import.meta.url, {
|
|
86
|
-
usage: 'Usage: inspect-artifacts.mjs --repo <dir> --group <groupId> --version <version> [--project <dir>] [--expect a,b,c]',
|
|
87
|
-
options: { repo: { type: 'string' }, group: { type: 'string' }, version: { type: 'string' }, project: { type: 'string' }, expect: { type: 'string' } },
|
|
86
|
+
usage: 'Usage: inspect-artifacts.mjs --repo <dir> --group <groupId> --version <version> [--project <dir>] [--expect a,b,c] [--unsigned-expected]',
|
|
87
|
+
options: { repo: { type: 'string' }, group: { type: 'string' }, version: { type: 'string' }, project: { type: 'string' }, expect: { type: 'string' }, 'unsigned-expected': { type: 'boolean', default: false } },
|
|
88
88
|
}, (v) => {
|
|
89
89
|
required(v, 'repo', 'group', 'version');
|
|
90
90
|
const expected = v.expect ? v.expect.split(',').map((s) => s.trim()).filter(Boolean) : v.project ? expectedFromProject(v.project, v.group) : [];
|
|
91
|
-
return inspectArtifacts({ repo: v.repo, group: v.group, version: v.version, expected });
|
|
91
|
+
return inspectArtifacts({ repo: v.repo, group: v.group, version: v.version, expected, unsignedExpected: v['unsigned-expected'] });
|
|
92
92
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Gradle init script for check-only staging on a machine without signing keys,
|
|
2
|
+
// for projects whose keys exist only in CI. It disables every Sign task, so the
|
|
3
|
+
// staged artifacts have no .asc files. Never use it with an upload task.
|
|
4
|
+
//
|
|
5
|
+
// ./gradlew publishToMavenLocal --init-script <path to this file> ...
|
|
6
|
+
allprojects {
|
|
7
|
+
tasks.withType<Sign>().configureEach { enabled = false }
|
|
8
|
+
}
|
|
@@ -57,7 +57,7 @@ export async function keyOnKeyserver(issuer, fetchImpl = fetch) {
|
|
|
57
57
|
return servers;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export async function verifySignatures({ repo, version, useGpg = gpgAvailable(), checkKeyserver = true, fetchImpl = fetch, verify = gpgVerify }) {
|
|
60
|
+
export async function verifySignatures({ repo, version, useGpg = gpgAvailable(), checkKeyserver = true, unsignedExpected = false, fetchImpl = fetch, verify = gpgVerify }) {
|
|
61
61
|
const checks = [];
|
|
62
62
|
const files = walk(repo).filter((f) => !isAuxiliary(basename(f)) && (!version || f.split(sep).includes(version)));
|
|
63
63
|
if (files.length === 0) return result([check('signatures.files', 'fail', `No artifacts found in ${repo}`)]);
|
|
@@ -76,9 +76,10 @@ export async function verifySignatures({ repo, version, useGpg = gpgAvailable(),
|
|
|
76
76
|
unreadable.push(`${relative(repo, f)}.asc (${err.message})`);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
checks.push(
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
if (!unsigned.length) checks.push(check('signatures.present', 'pass', `All ${files.length} file(s) have a .asc signature`));
|
|
80
|
+
else if (unsignedExpected) checks.push(check('signatures.present', 'warn', `${unsigned.length} of ${files.length} file(s) unsigned, as expected: signing runs in CI. Signatures are checked by the Portal on upload.`));
|
|
81
|
+
else checks.push(check('signatures.present', 'fail', `${unsigned.length} of ${files.length} file(s) have no .asc`, { files: unsigned.slice(0, 30), fix: 'errors.md E06.' }));
|
|
82
|
+
if (issuers.size === 0 && unreadable.length === 0) return result(checks, { fileCount: files.length, keys: [] });
|
|
82
83
|
if (unreadable.length) checks.push(check('signatures.format', 'fail', `${unreadable.length} signature file(s) could not be parsed`, { files: unreadable.slice(0, 30) }));
|
|
83
84
|
if (issuers.size > 1) checks.push(check('signatures.issuer', 'warn', `Files are signed by ${issuers.size} different keys`, { fix: 'Usually a mix of primary key and subkey, or two configured keys. See reference/signing-setup.md.' }));
|
|
84
85
|
|
|
@@ -115,9 +116,9 @@ export async function verifySignatures({ repo, version, useGpg = gpgAvailable(),
|
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
await runCli(import.meta.url, {
|
|
118
|
-
usage: 'Usage: verify-signatures.mjs --repo <dir> [--version <version>] [--no-gpg] [--no-keyserver]',
|
|
119
|
-
options: { repo: { type: 'string' }, version: { type: 'string' }, 'no-gpg': { type: 'boolean', default: false }, 'no-keyserver': { type: 'boolean', default: false } },
|
|
119
|
+
usage: 'Usage: verify-signatures.mjs --repo <dir> [--version <version>] [--no-gpg] [--no-keyserver] [--unsigned-expected]',
|
|
120
|
+
options: { repo: { type: 'string' }, version: { type: 'string' }, 'no-gpg': { type: 'boolean', default: false }, 'no-keyserver': { type: 'boolean', default: false }, 'unsigned-expected': { type: 'boolean', default: false } },
|
|
120
121
|
}, (v) => {
|
|
121
122
|
required(v, 'repo');
|
|
122
|
-
return verifySignatures({ repo: v.repo, version: v.version, useGpg: v['no-gpg'] ? false : gpgAvailable(), checkKeyserver: !v['no-keyserver'] });
|
|
123
|
+
return verifySignatures({ repo: v.repo, version: v.version, useGpg: v['no-gpg'] ? false : gpgAvailable(), checkKeyserver: !v['no-keyserver'], unsignedExpected: v['unsigned-expected'] });
|
|
123
124
|
});
|