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: 7 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ jobs:
ref: ${{ github.sha }}

# The kit's version is the tag without its v, or the job refuses: the tarball's name, the
# version `jpdev --version` prints and the version line of the runner template
# (templates/check.sh, line 2) must agree with the tag.
# version `jpdev --version` prints and the version lines of the vendored scripts must agree
# with the tag.
- name: Require the kit's version to equal the tag
id: version
shell: bash
Expand All @@ -154,6 +154,10 @@ jobs:
want="# from judgment-pack-devkit ${RELEASE_TAG} (templates/check.sh)"
[ "${line}" = "${want}" ] || { echo "templates/check.sh line 2 is '${line}'; the tag ${RELEASE_TAG} needs '${want}'" >&2; exit 1; }
cmp -s templates/check.sh scripts/dev/check.sh || { echo "scripts/dev/check.sh is not a copy of templates/check.sh" >&2; exit 1; }
line="$(sed -n 2p scripts/dev/handover_check.sh)"
want="# from judgment-pack-devkit ${RELEASE_TAG} (lib/handover_check.sh)"
[ "${line}" = "${want}" ] || { echo "scripts/dev/handover_check.sh line 2 is '${line}'; the tag ${RELEASE_TAG} needs '${want}'" >&2; exit 1; }
cmp -s <(tail -n +2 lib/handover_check.sh) <(tail -n +3 scripts/dev/handover_check.sh) || { echo "scripts/dev/handover_check.sh is not a versioned copy of lib/handover_check.sh" >&2; exit 1; }
echo "version=${version}" >> "${GITHUB_OUTPUT}"

# git archive writes the named paths of the commit, with the commit's time and each file's
Expand Down Expand Up @@ -184,7 +188,7 @@ jobs:
kit="${unpack}/judgment-pack-devkit-${VERSION}"
[ "$("${kit}/bin/jpdev" --version)" = "jpdev ${VERSION}" ] || { echo "the packaged jpdev does not report ${VERSION}" >&2; exit 1; }
"${kit}/bin/jpdev" env > /dev/null
for path in bin/jpdev lib/wrap.sh templates/check.sh templates/dev.schema.json scripts/sync-agent-docs.sh examples/judgment-pack-agent.dev.json LICENSE NOTICE README.md; do
for path in bin/jpdev lib/wrap.sh lib/handover_check.sh templates/check.sh templates/dev.schema.json scripts/dev/handover_check.sh scripts/sync-agent-docs.sh examples/judgment-pack-agent.dev.json LICENSE NOTICE README.md; do
test -e "${kit}/${path}" || { echo "the tarball does not hold ${path}" >&2; exit 1; }
done

Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ script uses the environment's interpreter and packages.
- `config/env.example`, `config/codex-words.txt`: the settings by example, and the base word list of
`jpdev codex check-brief`.
- `templates/dev.json`, `templates/dev.schema.json`: the manifest by example and its JSON Schema.
- `templates/check.sh`: the public runner a repository vendors as `scripts/dev/check.sh`.
- `templates/check.sh` and `lib/handover_check.sh`: the public checks a repository vendors as
`scripts/dev/check.sh` and `scripts/dev/handover_check.sh`.
- `templates/codex-brief.md`: the brief skeleton `jpdev codex brief-template` prints.
- `templates/AGENTS.md`, `templates/CLAUDE.md`: the agent instructions (one text, generated from
the other by `scripts/sync-agent-docs.sh`).
Expand All @@ -171,8 +172,8 @@ script uses the environment's interpreter and packages.
repository adopts the kit. `examples/judgment-pack-agent.dev.json`: a worked manifest.
- `tests/run.sh`: the kit's own tests (`tests/fixture/` holds a manifest of shell echoes and one
deliberate failure).
- `dev.json`, `scripts/dev/check.sh`, `AGENTS.md`, `CLAUDE.md`: the kit on itself, the list of checks
CI runs. `CONTRIBUTING.md`, `SECURITY.md`, `docs/releasing.md`: contributing, reporting and
releasing.
- `dev.json`, `scripts/dev/check.sh`, `scripts/dev/handover_check.sh`, `AGENTS.md`, `CLAUDE.md`: the
kit on itself, the list of checks CI runs, and the handover guard. `CONTRIBUTING.md`, `SECURITY.md`,
`docs/releasing.md`: contributing, reporting and releasing.

Apache-2.0; see `LICENSE`.
3 changes: 3 additions & 0 deletions bin/jpdev
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ cmd_setup() {
py_v=$("$py" --version 2>&1)
fi
local script=$root/scripts/dev/check.sh
if [ ! -x "$script" ] || [ ! -x "$root/scripts/dev/handover_check.sh" ]; then
jpdev_log "setup: adopting repositories vendor scripts/dev/check.sh and scripts/dev/handover_check.sh together"
fi
[ -x "$script" ] || script=$kit/templates/check.sh
JPDEV_LOGDIR=$JPDEV_SCRATCH/logs/$(basename "$root")-setup-$(date +%Y%m%dT%H%M%S)
export JPDEV_LOGDIR
Expand Down
40 changes: 30 additions & 10 deletions docs/adopting.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Adopting the kit in a repository

A repository adopts the kit with five files and one CI workflow. None of them depends on the
kit being installed: a contributor or CI runs the vendored runner with python3 alone, and a
machine that has the kit adds the caps and the lock around the same steps.
A repository adopts the kit with five files and two workflows. None of them depends on the kit
being installed: a contributor or CI runs the two vendored scripts directly, and a machine that
has the kit adds the caps and the lock around the same steps.

## 1. Write `dev.json`

Expand Down Expand Up @@ -41,16 +41,21 @@ in the summary block; more than three lines are counted and the last one is show

`jpdev check` validates the file against `templates/dev.schema.json` before it runs anything.

## 2. Vendor the runner
## 2. Vendor the check scripts

```sh
mkdir -p scripts/dev
cp templates/check.sh scripts/dev/check.sh
devkit=path/to/judgment-pack-devkit
cp "$devkit/templates/check.sh" scripts/dev/check.sh
cp "$devkit/scripts/dev/handover_check.sh" scripts/dev/handover_check.sh
```

Line 2 of the template, `# from judgment-pack-devkit vX.Y.Z (templates/check.sh)`, records the kit
release the copy came from. A vendored copy keeps that line, so a later reader can tell which release
it is behind; when you update the copy, copy the whole file again.
it is behind. The kit's `scripts/dev/handover_check.sh` similarly vendors `lib/handover_check.sh`
with its release on line 2. Vendor `scripts/dev/check.sh` and `scripts/dev/handover_check.sh`
together, and copy both whole files again when updating them. `jpdev setup` names this pair when
either file is absent.

`scripts/dev/check.sh [--setup] [--only A,B] [--skip A,B] [--all]` runs the manifest with no
caps and prints the summary block. `jpdev check` uses the vendored copy when it exists and
Expand All @@ -74,13 +79,28 @@ regenerates `CLAUDE.md`.

## 4. Add the CI workflow

Copy `templates/ci-check.yml` to `.github/workflows/dev-check.yml`. It reads the toolchain
Copy `templates/ci-check.yml` to `.github/workflows/ci.yml`. It reads the toolchain
versions from `dev.json`, sets the toolchains up, and runs `scripts/dev/check.sh --setup` and
`scripts/dev/check.sh --all`. Its handover job rejects removals from boundary paths unless the pull
request body contains `Boundary change: yes`, then runs `handover.schema_probe` when present. Copy
request body contains `Boundary change: yes`, then runs `handover.schema_probe` when present through
the vendored `scripts/dev/handover_check.sh`. Copy
`templates/update-branches.yml` to `.github/workflows/update-branches.yml` to update open branches
after a default-branch push and label conflict cases `carry needed`. A repository that already has
CI jobs keeps them and uses the workflow as the list of steps to move its checks into.
after a default-branch push and label conflict cases `carry needed`.

Set the optional `UPDATE_BRANCHES_TOKEN` secret to a fine-grained personal access token or GitHub
App installation token with repository contents and pull-requests write access. An update made with
that token starts CI through the resulting push. When the secret is absent, the workflow uses its
`GITHUB_TOKEN` and explicitly dispatches `ci.yml` for the updated pull-request branch, passing the
pull-request number so the `handover` job can read its base and body. The workflow's `actions: write`
permission permits that fallback dispatch. After adopting the workflows, check that an automatically
updated pull request shows the `check` and `handover` CI checks on its new head. A repository that
already has CI jobs keeps their required names and uses the template as the list of steps to move
into its `ci.yml`. The dispatch passes the updated head SHA, which `ci.yml` names in the run title
and checks out for both jobs.

The fallback cannot dispatch a fork's short branch name in the base repository. For an updated fork
pull request it comments that checks must be started by a push from the fork, then fails visibly;
the contributor must push from the fork to start the checks on that head.

## 5. Check it

Expand Down
7 changes: 5 additions & 2 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ the second.
1. Open a pull request that:
- sets `JPDEV_VERSION` in `lib/common.sh` to the release's version, and line 2 of
`templates/check.sh` to `# from judgment-pack-devkit v<version> (templates/check.sh)`, and
copies the template to `scripts/dev/check.sh` (a test holds the three together);
copies the template to `scripts/dev/check.sh`; sets line 2 of
`scripts/dev/handover_check.sh` to
`# from judgment-pack-devkit v<version> (lib/handover_check.sh)` and copies the rest from
`lib/handover_check.sh` (tests hold both vendored scripts to their sources and version);
- writes `docs/releases/<tag>.md`: what the release holds and what it does not. It is the text
the release page carries, and the first line ends with the tag's name, for example
`# jpdev v0.2.0`.
Expand Down Expand Up @@ -85,7 +88,7 @@ Every job works on the commit the run was started for, by its digest, not on the
[`ci.yml`](../.github/workflows/ci.yml): `lint` and `tests`. A check `main` has gained since that
commit is not run.
3. **Packages the kit.** It refuses unless `bin/jpdev --version` prints `jpdev <version>`, line 2 of
`templates/check.sh` names `<tag>`, and `scripts/dev/check.sh` is a copy of the template. Then
each vendored script names `<tag>`, and both scripts match their sources. Then
`git archive` of the commit into `judgment-pack-devkit-<version>.tgz`, `checksums.txt` written
over it and checked, and the unpacked tarball run once (`jpdev --version`, `jpdev env`).
4. **Attests and drafts.** Only if the tag still names the commit and no release exists under the
Expand Down
23 changes: 23 additions & 0 deletions scripts/dev/handover_check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# from judgment-pack-devkit v0.5.0 (lib/handover_check.sh)
set -eu

python3 - <<'PY'
import json, os, subprocess, sys

manifest = json.load(open("dev.json", encoding="utf-8"))
handover = manifest.get("handover", {})
base = os.environ["JPDEV_HANDOVER_BASE"]
if "Boundary change: yes" not in os.environ.get("JPDEV_PR_BODY", "").splitlines():
for path in handover.get("boundary_paths", []):
diff = subprocess.check_output(
["git", "diff", "--unified=0", base + "...HEAD", "--", path], text=True
)
removed = [line for line in diff.splitlines() if line.startswith("-") and not line.startswith("---")]
if removed:
print("handover: boundary path has removals: " + path, file=sys.stderr)
raise SystemExit(1)
probe = handover.get("schema_probe")
if probe:
subprocess.run(["bash", "-c", probe], check=True)
PY
45 changes: 40 additions & 5 deletions templates/ci-check.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
# Vendor as .github/workflows/dev-check.yml. Runs the repository's own check list (dev.json)
# Vendor as .github/workflows/ci.yml. Runs the repository's own check list (dev.json)
# with scripts/dev/check.sh, with the toolchains set up from the manifest and no caps.
name: dev-check
run-name: dev-check ${{ inputs.sha || github.sha }}

on:
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
sha:
description: Updated pull request head to check
required: true
type: string
pull_request:
description: Pull request whose updated head is being checked
required: true
type: string
workflow_call:
inputs:
sha:
required: false
type: string

permissions:
contents: read
pull-requests: read

jobs:
check:
Expand All @@ -19,6 +35,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ inputs.sha }}
- name: Read the toolchains from dev.json
id: dev
run: |
Expand Down Expand Up @@ -74,17 +91,35 @@ jobs:

handover:
name: handover
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ inputs.sha }}
- name: Read dispatched pull request metadata
if: github.event_name == 'workflow_dispatch'
id: pull
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const pullNumber = Number(context.payload.inputs.pull_request);
if (!Number.isSafeInteger(pullNumber) || pullNumber < 1) {
core.setFailed('pull_request must be a positive integer');
return;
}
const pull = await github.rest.pulls.get({
owner: context.repo.owner, repo: context.repo.repo, pull_number: pullNumber,
});
core.setOutput('base', pull.data.base.sha);
core.setOutput('body', pull.data.body || '');
- name: Check boundary paths and run the schema probe
env:
JPDEV_HANDOVER_BASE: ${{ github.event.pull_request.base.sha }}
JPDEV_PR_BODY: ${{ github.event.pull_request.body }}
run: lib/handover_check.sh
JPDEV_HANDOVER_BASE: ${{ github.event.pull_request.base.sha || steps.pull.outputs.base }}
JPDEV_PR_BODY: ${{ github.event.pull_request.body || steps.pull.outputs.body }}
run: scripts/dev/handover_check.sh
37 changes: 30 additions & 7 deletions templates/update-branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
workflow_call:

permissions:
actions: write
contents: write
pull-requests: write
issues: write
Expand All @@ -18,7 +19,10 @@ jobs:
steps:
- name: Update branches that are behind
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
USING_GITHUB_TOKEN: ${{ secrets.UPDATE_BRANCHES_TOKEN == '' }}
with:
github-token: ${{ secrets.UPDATE_BRANCHES_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
Expand All @@ -35,13 +39,6 @@ jobs:
await github.rest.pulls.updateBranch({
owner, repo, pull_number: pull.number, expected_head_sha: pull.head.sha,
});
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: pull.number, name: 'carry needed',
});
} catch (error) {
if (error.status !== 404) throw error;
}
} catch (error) {
if (error.status !== 409 && error.status !== 422) throw error;
await github.rest.issues.addLabels({
Expand All @@ -51,5 +48,31 @@ jobs:
owner, repo, issue_number: pull.number,
body: `Carry needed from base commit ${baseCommit}.`,
});
continue;
}
if (process.env.USING_GITHUB_TOKEN === 'true') {
let updated = pull;
for (let attempt = 0; attempt < 30; attempt += 1) {
await new Promise((resolve) => setTimeout(resolve, 1000));
updated = (await github.rest.pulls.get({owner, repo, pull_number: pull.number})).data;
if (updated.head.sha !== pull.head.sha) break;
}
if (updated.head.sha === pull.head.sha) throw new Error(`Pull request ${pull.number} did not expose its updated head`);
if (!updated.head.repo || updated.head.repo.full_name !== `${owner}/${repo}`) {
const message = 'Checks must be started by a push from the fork.';
await github.rest.issues.createComment({owner, repo, issue_number: pull.number, body: message});
throw new Error(message);
}
await github.rest.actions.createWorkflowDispatch({
owner, repo, workflow_id: 'ci.yml', ref: updated.head.ref,
inputs: { pull_request: String(pull.number), sha: updated.head.sha },
});
}
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: pull.number, name: 'carry needed',
});
} catch (error) {
if (error.status !== 404) throw error;
}
}
Loading
Loading