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
117 changes: 117 additions & 0 deletions .github/workflows/publish-retry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: publish-retry

# Re-runs ONLY the external-registry publish steps for an EXISTING release tag — for when
# release.yml succeeded through the GitHub Release but a downstream publish step failed
# (first use: v2.0.0, where central-publishing-maven-plugin 0.5.0 choked on the Portal API
# response after the library bundle had already uploaded). Safe to re-run: Central uploads
# are USER_MANAGED (nothing goes live without a manual Publish click; duplicate pending
# deployments can be dropped in the Portal UI), and the Plugin Portal rejects re-publishing
# an existing version rather than overwriting it.

on:
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to publish from (e.g. v2.0.0)"
required: true
type: string
maven_central:
description: "Publish the maven-plugin to Maven Central"
required: false
type: boolean
default: true
plugin_portal:
description: "Publish the Gradle plugin to the Plugin Portal"
required: false
type: boolean
default: true
pom_from_default_branch:
description: "Use the default branch's maven-plugin/pom.xml (publish-mechanics fixes) instead of the tag's — artifact version/code still come from the tag"
required: false
type: boolean
default: false

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
env:
# secrets are not readable in step `if:` expressions — surface presence as booleans here.
HAS_CENTRAL_CREDS: ${{ secrets.MAVEN_CENTRAL_USERNAME != '' && secrets.MAVEN_CENTRAL_PASSWORD != '' && secrets.SIGNING_KEY != '' && secrets.SIGNING_PASSWORD != '' }}
HAS_PORTAL_CREDS: ${{ secrets.GRADLE_PUBLISH_KEY != '' }}
steps:
- name: Resolve tag
id: ver
env:
INPUT_TAG: ${{ inputs.tag }}
run: |
if [[ ! "$INPUT_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([._+-][A-Za-z0-9._+-]*)?$ ]]; then
echo "::error::'$INPUT_TAG' is not a v<semver> tag."
exit 1
fi
echo "tag=$INPUT_TAG" >> "$GITHUB_OUTPUT"
echo "version=${INPUT_TAG#v}" >> "$GITHUB_OUTPUT"

- uses: actions/checkout@v4
with:
ref: refs/tags/${{ steps.ver.outputs.tag }}
# fetch-depth 0 so the default branch is available for pom_from_default_branch below.
fetch-depth: 0

# Opt-in escape hatch: a tag can pin publish MECHANICS that later turn out broken (v2.0.0
# pinned central-publishing-maven-plugin 0.5.0, which cannot parse current Portal API
# responses). This swaps in ONLY maven-plugin/pom.xml from the default branch — the
# artifact's version and code still come from the tag checkout.
- name: Use default-branch maven-plugin pom (publish-mechanics fix)
if: ${{ inputs.pom_from_default_branch }}
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
git checkout "origin/$DEFAULT_BRANCH" -- maven-plugin/pom.xml
echo "maven-plugin/pom.xml taken from origin/$DEFAULT_BRANCH"

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
cache: gradle

- name: Publish maven-plugin to Maven Central
if: ${{ inputs.maven_central && env.HAS_CENTRAL_CREDS == 'true' }}
env:
VERSION: ${{ steps.ver.outputs.version }}
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
run: |
# The maven-plugin resolves io.github.beltian.pjacoco:pjacoco-agent from mavenLocal.
./gradlew --no-daemon "-PreleaseVersion=$VERSION" :agent:publishToMavenLocal
printf '%s' "$SIGNING_KEY" | gpg --batch --import
mkdir -p ~/.m2
cat > ~/.m2/settings.xml <<'SETTINGS'
<settings>
<servers>
<server>
<id>central</id>
<username>${env.MAVEN_CENTRAL_USERNAME}</username>
<password>${env.MAVEN_CENTRAL_PASSWORD}</password>
</server>
</servers>
</settings>
SETTINGS
export MAVEN_GPG_PASSPHRASE="$SIGNING_PASSWORD"
mvn -B -ntp -f maven-plugin/pom.xml -Pcentral -DskipTests \
"-Dpjacoco.agent.version=$VERSION" deploy

- name: Publish Gradle plugin to the Plugin Portal
if: ${{ inputs.plugin_portal && env.HAS_PORTAL_CREDS == 'true' }}
env:
VERSION: ${{ steps.ver.outputs.version }}
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
run: |
./gradlew --no-daemon "-PreleaseVersion=$VERSION" :gradle-plugin:publishPlugins
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# pjacoco v1.3.0 멀티 프로젝트 커버리지 검증 결과

- 측정일: 2026-06-20
- 도구: pjacoco **v1.3.0** (`pjacoco-agent-1.3.0.jar`, maven local / GitHub Release v1.3.0) + `jacococli 0.8.13`
- 비교 기준: vanilla JaCoCo 0.8.13, 그리고 직전 **v1.1.0** sweep 결과(`results-v1.1.0-baseline.jsonl`)
- 방식: 빌드 파일 **무수정** 비침습 `-javaagent` 부착 — Maven `-DargLine`, Gradle init script로 주입,
모듈별 `aggregate.exec` → `jacococli merge` → CSV 리포트
- 대상: `~/github_*`의 Java(Maven/Gradle) 프로젝트 **23개**

## 핵심 결론

**pjacoco v1.3.0은 커버리지를 올바르게 도출한다.** 두 축으로 검증했다.

1. **정확성(vanilla 동일성) — 결정적 증거.** spring-petclinic에서 pjacoco v1.3.0의 `aggregate.exec`이
vanilla JaCoCo 0.8.13과 **클래스 단위까지 byte-identical**.
2. **회귀 없음(v1.1.0 동일성).** test가 실제로 실행된 모든 프로젝트에서 v1.3.0 커버리지가 v1.1.0과 일치.

## 1. 정확성 검증 — vanilla JaCoCo 0.8.13 동일성 (spring-petclinic)

동일 includes(`org.springframework.samples.*`)·동일 테스트로 vanilla agent와 v1.3.0 agent를 각각 부착해
산출한 `.exec`을 같은 classfiles로 리포트, 클래스별로 대조.

| 지표 | vanilla 0.8.13 | pjacoco v1.3.0 | 일치 |
|---|---|---|---|
| INSTRUCTION (cov/miss) | 2261 / 638 | 2261 / 638 | ✅ |
| LINE (cov/miss) | 545 / 115 | 545 / 115 | ✅ |
| METHOD (cov/miss) | 186 / 34 | 186 / 34 | ✅ |
| BRANCH (cov/miss) | 144 / 109 | 144 / 109 | ✅ |
| 분석 클래스 수 | 45 | 45 | ✅ |
| per-class 차이 | — | **0** | ✅ |

→ **EQUIVALENCE: PASS** (only_vanilla=0, only_pjacoco=0, per_class_diffs=0). pjacoco의 additive 라우팅이
vanilla 측정값을 그대로 보존함을 재확인.

## 2. maven-plugin 1.3.0 per-test 경로 검증

- **배포 상태 확인.** v1.3.0 maven-plugin은 **maven local에 미설치**(1.0.0/1.1.0만 존재)였고, **GitHub
Release v1.3.0에 `pjacoco-maven-plugin-1.3.0.jar` 자산으로 존재**. 레포 소스(`maven-plugin/pom.xml`,
version 1.3.0)에서 `mvn -f maven-plugin/pom.xml install`로 maven local에 설치 완료.
- **전체 toolchain 확보:** agent · testkit(4종) · gradle-plugin · maven-plugin **모두 1.3.0**.
- **per-test 동작 검증.** `samples/maven-sample`(이미 `pjacoco.version=1.3.0` 고정)을 `mvn test`로 실행 →
per-test `T1.exec` + `aggregate.exec` 생성. 격리 확인:
- `T1.exec`(단일 테스트 슬라이스): `Calc` INSTRUCTION_COVERED **6**, METHOD 1
- `aggregate.exec`(전체 실행): `Calc` INSTRUCTION_COVERED **9**, METHOD 2
- → per-test ⊊ aggregate 관계 성립 = 테스트별 커버리지가 단일 테스트로 정확히 스코프됨.

## 3. 회귀 검증 — v1.1.0 baseline 동일성 (23개 sweep)

`-javaagent`만 v1.1.0→v1.3.0으로 교체하고 동일 하니스로 재측정한 결과를 baseline과 대조.

표기: `instr% / covered_lines - total_lines`

| 프로젝트 | 도구 | v1.1.0 | v1.3.0 | 판정 |
|---|---|---|---|---|
| spring-petclinic-maven | maven | 78.0% / 545-660 | 78.0% / 545-660 | MATCH |
| tainted-spring-auth-user | maven | 90.7% / 225-251 | 90.7% / 225-251 | MATCH |
| tainted-spring-bff-gateway | gradle | 85.2% / 100-119 | 85.2% / 100-119 | MATCH† |
| jdbc-intercept-agent | gradle | 84.3% / 332-400 | 84.3% / 332-400 | MATCH† |
| tainted-spring-mindgraph | maven | 83.0% / 223-274 | 83.0% / 223-274 | MATCH |
| tainted-spring-notification | gradle | 78.1% / 60-83 | 78.1% / 60-83 | MATCH† |
| vcsstatistics | maven | 76.4% / 2444-3102 | 76.4% / 2444-3102 | MATCH |
| spring-mutation-tests | gradle | 59.8% / 733-1220 | 59.8% / 733-1218 | MATCH‡ |
| line-service | maven | 39.6% / 16-37 | 39.6% / 16-37 | MATCH |
| tainted-spring-community | maven | 36.1% / 156-249 | 36.1% / 156-249 | MATCH |
| member-service | maven | 13.5% / 22-170 | 13.5% / 22-170 | MATCH§ |
| bytebuddy-example | maven | 7.9% / 2-24 | 7.9% / 2-24 | MATCH |
| assurenet | maven | 0.0% / 6-4498 | 0.0% / 6-4498 | MATCH |
| jpashop | maven | 0.0% / 0-315 | 0.0% / 0-315 | MATCH |
| spring-microservice-sample | maven | 0.0% / 0-451 | 0.0% / 0-451 | MATCH |
| hotspot-analysis | gradle | 0.0% / 0-2757 | 0.0% / 0-2757 | MATCH† |
| (빌드불가/테스트없음 6종) | — | null | null | MATCH |
| rbm-server | gradle | 0.2% / 11-3057 | (BUILD FAILED) | 환경¶ |

- **MATCH 21/23.** 의미 있는 커버리지를 내던 프로젝트 전부 v1.1.0과 동일.
- † **gradle 캐시 주의.** 첫 sweep에서 5개 gradle 프로젝트가 `test UP-TO-DATE`(직전 실행 캐시)로
재실행되지 않아 `aggregate.exec`이 비어 null로 나왔다. `--rerun-tasks`로 강제 재실행하니 모두 v1.1.0과
동일하게 산출 — **pjacoco가 아니라 Gradle 증분 빌드 캐시**가 원인(하니스 한계).
- ‡ spring-mutation-tests: `instr% 59.8%`·`covered_lines 733` **동일**, 분모 `total_lines`만 1220→1218
(로드된 클래스 집합 비결정성). 실행된 코드의 커버리지는 동일.
- § member-service: 1차 측정 때 외부 MockTcpServer(port 9999) 대기로 600s timeout → 0(null)이었으나,
포트가 빈 상태로 **재실행하니 13.5% / 22-170로 v1.1.0과 정확히 일치**. null은 일시적 환경 행오버였다.
- ¶ rbm-server: Gradle 7.3 deprecation으로 BUILD FAILED — **프로젝트 자체 빌드 문제**이며 pjacoco 무관
(v1.1.0에서도 0.2%로 사실상 미산출).

## 결론

- v1.3.0 agent는 vanilla JaCoCo와 **byte 단위로 동일한** 커버리지를 산출한다(petclinic, 45클래스 0-diff).
- v1.1.0 대비 **회귀 없음** — test가 실제 실행된 모든 프로젝트(21개)에서 수치 일치.
- v1.3.0 **maven-plugin 설치 경로**(GitHub Release → maven local)와 **per-test 격리**(maven-sample)도 정상.
- null/실패의 원인은 전부 **Gradle 캐시(하니스)·외부 인프라 의존(프로젝트)**이지 pjacoco 결함이 아니다.

## 재현 방법

```bash
# 0. v1.3.0 agent 설치 (maven local) + maven-plugin 1.3.0 설치
# agent/testkit/gradle-plugin: ./gradlew :agent:publishToMavenLocal ... :gradle-plugin:publishToMavenLocal
# maven-plugin: mvn -f maven-plugin/pom.xml install
# 1. 비침습 측정 (대상 프로젝트 빌드파일 무수정)
~/.pjacoco-measure/measure.sh <project-root> <name> [includes] # agent.jar = pjacoco-agent-1.3.0.jar
# 2. vanilla 동일성 (petclinic)
~/.pjacoco-measure/equiv-petclinic.sh && ~/.pjacoco-measure/compare-equiv.sh
# 3. gradle 캐시 회피 재측정
~/.pjacoco-measure/remeasure-gradle.sh <root> <name> <includes> <JAVA_HOME> # --rerun-tasks
```
63 changes: 63 additions & 0 deletions docs/feedback/2026-06-20-pjacoco-v1.3.0-적용-피드백.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# pjacoco v1.3.0 적용 피드백 (멀티 프로젝트 검증 중 발견)

- 작성일: 2026-06-20
- 맥락: `~/github_*` Java 23개 프로젝트에 v1.3.0 적용·검증(→ [검증결과](./2026-06-20-pjacoco-v1.3.0-멀티프로젝트-검증결과.md))
과정에서 드러난 **사용성·DX 이슈**. 측정 정확성 자체는 정상(vanilla와 byte-identical)이며, 아래는 별개의
개선 후보다.
- 기존 피드백(P1~P3): [`2026-06-20-pjacoco-사용성-피드백.md`](./2026-06-20-pjacoco-사용성-피드백.md)

## P4-A (Medium) — Gradle 플러그인이 커버리지 출력을 Test 태스크 입출력으로 선언하지 않아, `UP-TO-DATE` 캐시 시 `.exec`가 무음으로 사라짐

**증상.** 에이전트를 붙인 `test` 태스크가 Gradle 증분 캐시로 `UP-TO-DATE`가 되면, per-test/aggregate
`.exec`이 **재생성되지 않는데 BUILD는 SUCCESSFUL**이라 사용자는 커버리지가 비어 있는 줄 모른다. 멀티
프로젝트 sweep에서 5개 gradle 프로젝트가 이 이유로 커버리지 0/null로 나왔고(`--rerun-tasks` 강제
재실행하니 정상), pjacoco가 아니라 캐시가 원인임을 추적해야 했다.

**재현 (공식 플러그인 경로, Gradle 9.5.1, `samples/gradle-sample`):**

```
# run1: clean test
gradle clean test # → build/pjacoco/aggregate.exec 생성 ✅
rm build/pjacoco/*.exec # 출력만 삭제
# run2: test 재실행
gradle test # → Task :test UP-TO-DATE, BUILD SUCCESSFUL
find . -name 'aggregate.exec' # → (없음) ❌ 커버리지 무음 손실
```

**근본 원인.** `PjacocoPlugin.AgentArgs implements CommandLineArgumentProvider`이지만 `@Input`
애너테이션이 없고(`gradle-plugin/.../PjacocoPlugin.java:83`), `build/pjacoco/` 출력 디렉터리도 Test
태스크의 `@OutputDirectory`/`outputs.dir(...)`로 등록되지 않는다. 그래서 Gradle의 up-to-date 판정이
(1) pjacoco on/off·옵션 변경을 입력 변화로 인지하지 못하고 (2) 커버리지 출력 삭제를 출력 변화로 인지하지
못한다. 바닐라 JaCoCo의 `JacocoTaskExtension`은 exec 목적지를 태스크 출력으로 묶어 이 문제가 없다.

**제안.** 에이전트 jar/args를 태스크 입력(`@Input`/`@Classpath`)으로, `build/pjacoco`를
`@OutputDirectory`(또는 `test.outputs.dir(...)`)로 선언해 JaCoCo와 동일하게 캐시에 참여시킨다. 최소 가드로
`test.outputs.upToDateWhen { false }`도 가능하나 캐시 이점을 잃으므로 입출력 선언이 정석.

**영향.** `clean` 없이 `gradle test`를 재실행하는 일반적 흐름에서 커버리지가 조용히 비거나 낡은 채로
남는다 — "0%"·"데이터 없음"으로 오인. CI에서 캐시가 살아 있으면 특히 위험.

## P4-B (Low) — maven-plugin 1.3.0가 온보딩/릴리스 경로에서 maven local에 자동 설치되지 않음

**증상.** v1.3.0 측정 시작 시 maven local에는 agent·testkit(4종)·gradle-plugin은 **1.3.0**이 있었으나
**maven-plugin은 1.1.0**까지뿐이라, `io.pjacoco:pjacoco-maven-plugin:1.3.0` 좌표가 resolve되지 않았다.
GitHub Release v1.3.0에는 `pjacoco-maven-plugin-1.3.0.jar`가 자산으로 있어, 레포 소스에서
`mvn -f maven-plugin/pom.xml install`로 수동 설치해야 했다.

**근본 원인.** `release.yml`은 `:agent`+`testkit×4`만 `publishToMavenLocal`하고(L83–85), maven-plugin은
`package` 후 자산 복사만(L87, L102) 한다 — `install` 없음. gradle-plugin도 그 목록에 없어, 두 **플러그인**의
로컬 설치는 README의 별도 수동 단계(`:gradle-plugin:publishToMavenLocal`, `mvn ... install`)에 의존한다.
이 2단계는 절반만 완료되기 쉽다(gradle 단계만 돌리고 maven 단계를 잊으면 maven-plugin이 낡은 채 남음 —
이번에 정확히 그 상태였다).

**제안.** "로컬 전체 설치"를 한 번에 하는 집계 태스크(예: `:installAllToMavenLocal`)에 maven-plugin
`mvn install`까지 포함하거나, README 빠른 시작의 두 명령을 하나의 스크립트(`scripts/install-local.sh`)로
묶어 부분 설치를 방지한다.

**영향.** 온보딩 마찰 + 낡은 플러그인이 조용히 사용될 위험. 공개 배포(REQ-D03) 완료 시 자연 해소되나,
그 전까지의 로컬 설치 UX 문제.

## 메모 — pjacoco 무관(프로젝트/환경 사정, 참고용)

- member-service: 외부 MockTcpServer(port 9999) 점유 시 테스트가 600s 행 → 0; 포트 free면 13.5%로 정상.
- rbm-server: Gradle 7.3 deprecation으로 BUILD FAILED(빌드 자체 문제).
Loading
Loading