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
10 changes: 10 additions & 0 deletions .changeset/fix-trusted-publishing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'lino-arguments': patch
---

fix: add repository field to package.json for npm trusted publishing

This fixes the E422 error when publishing with npm trusted publishing (OIDC).
npm provenance verification requires the repository.url in package.json to match
the Source Repository URI in the signing certificate. Without this field, the
validation fails with "repository.url is '', expected to match 'https://github.com/link-foundation/lino-arguments'".
2,223 changes: 2,223 additions & 0 deletions docs/case-studies/ci-logs/run-20054176340.txt

Large diffs are not rendered by default.

202 changes: 202 additions & 0 deletions docs/case-studies/ci-logs/run-20054499396.txt

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions docs/case-studies/ci-runs-list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
[
{
"conclusion": "failure",
"createdAt": "2025-12-09T06:43:29Z",
"databaseId": 20054499396,
"headSha": "9b140e10cd5536fe725a8bae35cbde9ef2d5559a",
"name": "CI/CD"
},
{
"conclusion": "failure",
"createdAt": "2025-12-09T06:27:13Z",
"databaseId": 20054176340,
"headSha": "b03321d9bf12abc38db253473b516d0c78d1e0c5",
"name": "CI/CD"
},
{
"conclusion": "success",
"createdAt": "2025-12-09T05:51:18Z",
"databaseId": 20053465387,
"headSha": "263b92fb860163ccf9fd421839123f53e96923bf",
"name": "CI/CD"
},
{
"conclusion": "success",
"createdAt": "2025-12-09T05:49:30Z",
"databaseId": 20053432311,
"headSha": "b3cbcd490a031ef982de68baaa73acbcbe1dcf9f",
"name": "CI/CD"
},
{
"conclusion": "failure",
"createdAt": "2025-12-09T05:47:55Z",
"databaseId": 20053403313,
"headSha": "673f5baa479f9bbf9caa193acfc1c4eb3a440caa",
"name": "CI/CD"
},
{
"conclusion": "failure",
"createdAt": "2025-12-09T05:39:49Z",
"databaseId": 20053254429,
"headSha": "ba5978d15ddf54d27dd50f6c2294fa00c8ff9e9d",
"name": "CI/CD"
},
{
"conclusion": "success",
"createdAt": "2025-12-09T04:38:49Z",
"databaseId": 20052139499,
"headSha": "6e61d2051425b83db531df2061f6ab205df6f701",
"name": "CI/CD"
},
{
"conclusion": "success",
"createdAt": "2025-12-09T04:36:45Z",
"databaseId": 20052105671,
"headSha": "908aa6912b83ff95ca0248cbcc0865937f5ec535",
"name": "CI/CD"
},
{
"conclusion": "failure",
"createdAt": "2025-12-09T04:35:09Z",
"databaseId": 20052080496,
"headSha": "e3dbc593c9ec1acbad99f1c7cd34dfd03158e82e",
"name": "CI/CD"
},
{
"conclusion": "failure",
"createdAt": "2025-12-09T04:34:13Z",
"databaseId": 20052064430,
"headSha": "5479aabd7352a5e2a8c49fd56dc0c3d2acf919c1",
"name": "CI/CD"
}
]
Binary file added docs/case-studies/npm-config-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
219 changes: 219 additions & 0 deletions docs/case-studies/trusted-publishing-failure-case-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# Case Study: npm Trusted Publishing E422 Error

## Issue Reference

- **Issue**: [#10 - Trusted publishing does not work in our CI/CD](https://github.com/link-foundation/lino-arguments/issues/10)
- **CI Run**: [20054176340](https://github.com/link-foundation/lino-arguments/actions/runs/20054176340/job/57515987959)
- **Date**: 2025-12-09

## Executive Summary

The npm package `lino-arguments` failed to publish via GitHub Actions with OIDC trusted publishing due to a missing `repository` field in `package.json`. npm's provenance verification requires the `repository.url` in `package.json` to match the repository URL from the provenance signature, but since the field was empty, the validation failed with an E422 error.

## Timeline of Events

| Time (UTC) | Event |
| ------------------- | ---------------------------------------------------------- |
| 2025-12-09 06:27:13 | CI/CD workflow triggered on main branch (commit `b03321d`) |
| 2025-12-09 06:27:20 | Tests and linting passed successfully |
| 2025-12-09 06:27:52 | Changeset version bump to 0.2.4 committed |
| 2025-12-09 06:27:53 | npm publish initiated with OIDC trusted publishing |
| 2025-12-09 06:27:55 | Provenance statement signed and published to Sigstore |
| 2025-12-09 06:27:55 | **E422 Error**: Repository URL validation failed |
| 2025-12-09 06:27:55 | Publish failed, workflow exited with error |

## Error Analysis

### Exact Error Message

```
E422 422 Unprocessable Entity - PUT https://registry.npmjs.org/lino-arguments -
Error verifying sigstore provenance bundle: Failed to validate repository information:
package.json: "repository.url" is "", expected to match "https://github.com/link-foundation/lino-arguments" from provenance
```

### Error Code Breakdown

| Component | Value |
| ------------------ | ------------------------------------------------------------ |
| HTTP Status | 422 Unprocessable Entity |
| npm Error Code | E422 |
| Endpoint | `PUT https://registry.npmjs.org/lino-arguments` |
| Sigstore Log Index | [752580455](https://search.sigstore.dev/?logIndex=752580455) |

### What Worked vs What Failed

| Component | Status | Evidence |
| ------------------------------- | ---------- | ------------------------------------------------------------ |
| GitHub Actions workflow | Working | All tests passed |
| OIDC token generation | Working | id-token: write permission granted |
| npm authentication via OIDC | Working | npm received valid OIDC token |
| Provenance statement signing | Working | Published to Sigstore transparency log |
| Trusted publisher config on npm | Working | Screenshot shows `link-foundation/lino-arguments` configured |
| Repository URL validation | **FAILED** | package.json missing repository field |

## Root Cause Analysis

### The Core Problem

npm's trusted publishing feature performs server-side validation to ensure the `repository.url` field in `package.json` matches the Source Repository URI extension in the signing certificate. This is a security measure to prevent packages from claiming provenance from a repository they don't actually belong to.

**The `package.json` was missing the `repository` field entirely**, causing the validation to compare an empty string `""` against the expected URL `https://github.com/link-foundation/lino-arguments`.

### Why This Matters for Security

npm provenance attestations allow consumers to verify:

1. The package was built from a specific repository
2. The build process was transparent and reproducible
3. The package hasn't been tampered with

Without the `repository` field matching, npm cannot guarantee that the provenance claim is legitimate.

### Evidence from package.json

```json
{
"name": "lino-arguments",
"version": "0.2.4",
"description": "A setup of Links Notation Environment...",
// ... other fields ...
"author": "",
"license": "Unlicense"
// NOTE: No "repository" field present!
}
```

## npm Trusted Publishing: How It Works

### OIDC Authentication Flow

```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ GitHub Actions │───▶│ npm OIDC │───▶│ npm Registry │
│ (Workflow) │ │ Provider │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ 1. Request │ 2. Generate │
│ OIDC token │ JWT with │
│ │ claims │
│ │ │
└──────────────────────┴──────────────────────┘
3. Publish with
provenance
┌─────────────────┐
│ Sigstore │
│ Transparency │
│ Log │
└─────────────────┘
```

### Validation Points

1. **OIDC Token Claims**: Contains repository, workflow, and actor information
2. **Provenance Bundle**: Signed attestation of build origin
3. **Repository URL Check**: `package.json` repository must match provenance claims
4. **Trusted Publisher Config**: npm must have the repository registered

## Related Issues and References

### npm CLI Bug #8036

- **URL**: https://github.com/npm/cli/issues/8036
- **Issue**: Capitalization mismatch in repository URLs causes E422
- **Resolution**: Ensure exact URL match including case sensitivity

### npm Documentation

- [Trusted Publishers](https://docs.npmjs.com/trusted-publishers/)
- [Generating Provenance Statements](https://docs.npmjs.com/generating-provenance-statements/)

### Key Requirements for npm Provenance

Per [npm/provenance documentation](https://github.com/npm/provenance):

1. `package.json` must include `repository` or `repository.url` field
2. URL must exactly match the GitHub repository (case-sensitive)
3. Workflow must have `id-token: write` permission
4. Must be publishing from a public repository

## Solution

### Required Change

Add the `repository` field to `package.json`:

```json
{
"repository": {
"type": "git",
"url": "https://github.com/link-foundation/lino-arguments.git"
}
}
```

Or the shorthand version:

```json
{
"repository": "github:link-foundation/lino-arguments"
}
```

### Why This Fixes the Issue

1. npm can now extract `https://github.com/link-foundation/lino-arguments` from the repository field
2. This matches the Source Repository URI in the provenance signature
3. The E422 validation passes
4. Package publishes successfully with provenance attestation

## Configuration Verification

### npm Settings (from screenshot)

| Setting | Value |
| ----------------- | ------------------------------ |
| Package Name | lino-arguments |
| Version | 0.2.1 (before this incident) |
| Access | Public |
| Trusted Publisher | link-foundation/lino-arguments |
| Workflow | main.yml |
| 2FA | Not required |

### GitHub Workflow Settings

| Setting | Value |
| -------------------- | -------------------------- |
| Workflow File | .github/workflows/main.yml |
| Permission: id-token | write |
| Permission: contents | write |
| npm upgrade | Yes (npm@latest) |

## Lessons Learned

1. **Always include `repository` in package.json**: This is required for npm provenance and helps package consumers find the source code.

2. **Test provenance locally first**: Use `npm pack --dry-run` and check the package metadata before publishing.

3. **Verify exact URL match**: Repository URLs are case-sensitive and must match exactly what GitHub reports.

4. **The error message is informative**: The E422 error clearly states what's expected vs what was found.

## Files in This Case Study

- `ci-logs/run-20054176340.txt` - Full CI logs from the failing run
- `ci-logs/run-20054499396.txt` - Subsequent PR check run logs
- `ci-runs-list.json` - List of recent CI runs
- `npm-config-screenshot.png` - npm package settings showing trusted publisher config

## References

1. [npm Trusted Publishers Documentation](https://docs.npmjs.com/trusted-publishers/)
2. [npm Provenance Documentation](https://docs.npmjs.com/generating-provenance-statements/)
3. [npm/provenance GitHub Repository](https://github.com/npm/provenance)
4. [npm CLI Issue #8036](https://github.com/npm/cli/issues/8036)
5. [GitHub Blog: npm trusted publishing with OIDC](https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/)
6. [Sigstore Transparency Log Entry](https://search.sigstore.dev/?logIndex=752580455)
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
],
"author": "",
"license": "Unlicense",
"repository": {
"type": "git",
"url": "https://github.com/link-foundation/lino-arguments.git"
},
"engines": {
"node": ">=20.0.0"
},
Expand Down