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
6 changes: 4 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ jobs:
name: Build demo site
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 11.27.0
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 24
cache: pnpm
Expand Down
230 changes: 230 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
name: Publish to npm

on:
workflow_dispatch:
inputs:
dist_tag:
description: 'npm dist-tag(next = RC / latest = 正式版)'
type: choice
options: [next, latest]
required: true
expected_version:
description: '公開する version(package.json と一致すること。例 1.0.1-rc.1)'
type: string
required: true
expected_sha:
description: '起動する tag が指す commit の SHA(40 桁)'
type: string
required: true
dry_run:
description: 'true なら publish しない(リハーサル)'
type: boolean
default: true

permissions: {}

concurrency:
group: npm-publish
cancel-in-progress: false

env:
# ---- repo ごとに変える(設計書 §3.2 の表) ----
CI_WORKFLOW_FILE: test.yml
RELEASE_CHECK: pnpm run prepublishOnly
# ---- 共通 ----
PNPM_VERSION: 11.27.0
NODE_VERSION: '24'

jobs:
verify:
name: 事前検査・build・pack(OIDC 権限なし)
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
actions: read
outputs:
name: ${{ steps.meta.outputs.name }}
version: ${{ steps.meta.outputs.version }}
tarball: ${{ steps.pack.outputs.tarball }}
sha256: ${{ steps.pack.outputs.sha256 }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: 起動条件の検査
id: meta
env:
DIST_TAG: ${{ inputs.dist_tag }}
EXPECTED_VERSION: ${{ inputs.expected_version }}
EXPECTED_SHA: ${{ inputs.expected_sha }}
DRY_RUN: ${{ inputs.dry_run }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# 形式検査(Minor #2): expected_version は semver、expected_sha は 40 桁の 16 進数。誤入力・注入文字列を早期に fail する
VER_RE='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'
SHA_RE='^[0-9a-fA-F]{40}$'
[[ "${EXPECTED_VERSION}" =~ ${VER_RE} ]] || { echo "::error::expected_version が semver でない: ${EXPECTED_VERSION}"; exit 1; }
[[ "${EXPECTED_SHA}" =~ ${SHA_RE} ]] || { echo "::error::expected_sha が 40 桁の 16 進数でない"; exit 1; }
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
echo "name=${NAME}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
# 本番は全項目で停止。dry_run は SHA 以外を警告に留める(master でのリハーサルのため)
check() { if [ "${DRY_RUN}" = "true" ]; then echo "::warning::$1"; else echo "::error::$1"; exit 1; fi; }
[ "${GITHUB_SHA}" = "${EXPECTED_SHA}" ] || { echo "::error::SHA 不一致: ${GITHUB_SHA} != ${EXPECTED_SHA}"; exit 1; }
[ "${GITHUB_REF_TYPE}" = "tag" ] || check "tag 以外から起動された(${GITHUB_REF})"
[ "${GITHUB_REF_NAME}" = "v${EXPECTED_VERSION}" ] || check "tag 名 ${GITHUB_REF_NAME} が v${EXPECTED_VERSION} と一致しない"
[ "${VERSION}" = "${EXPECTED_VERSION}" ] || check "version 不一致: package.json=${VERSION} 入力=${EXPECTED_VERSION}"
case "${VERSION}" in
*-*) [ "${DIST_TAG}" = "next" ] || check "prerelease ${VERSION} を ${DIST_TAG} に出そうとしている" ;;
*) [ "${DIST_TAG}" = "latest" ] || check "正式版 ${VERSION} を ${DIST_TAG} に出そうとしている" ;;
esac
if [ -n "$(npm view "${NAME}@${VERSION}" version 2>/dev/null || true)" ]; then
check "${NAME}@${VERSION} は公開済み"
fi
N=$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/${CI_WORKFLOW_FILE}/runs?head_sha=${GITHUB_SHA}&status=success" --jq '.total_count')
[ "${N}" -ge 1 ] || check "${CI_WORKFLOW_FILE} が ${GITHUB_SHA} で成功していない"
{
echo "### publish 候補"
echo "- package: \`${NAME}@${VERSION}\` → dist-tag \`${DIST_TAG}\`(dry_run=${DRY_RUN})"
echo "- ref: \`${GITHUB_REF}\` / sha: \`${GITHUB_SHA}\`"
} >> "$GITHUB_STEP_SUMMARY"

- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: ${{ env.PNPM_VERSION }}

- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm

- name: CI 用 pnpm 設定を展開
run: cp ci/pnpm-workspace.ci.yaml pnpm-workspace.yaml

- name: 依存の導入
run: pnpm install --frozen-lockfile

- name: release 検査(lint・typecheck・test・build)
run: bash -euo pipefail -c "${RELEASE_CHECK}"

- name: pack
id: pack
run: |
set -euo pipefail
mkdir -p "${RUNNER_TEMP}/pack"
pnpm pack --pack-destination "${RUNNER_TEMP}/pack"
COUNT=$(find "${RUNNER_TEMP}/pack" -maxdepth 1 -name '*.tgz' | wc -l)
[ "${COUNT}" -eq 1 ] || { echo "::error::tarball が ${COUNT} 個"; exit 1; }
T=$(find "${RUNNER_TEMP}/pack" -maxdepth 1 -name '*.tgz')
S=$(sha256sum "${T}" | cut -d' ' -f1)
echo "tarball=$(basename "${T}")" >> "$GITHUB_OUTPUT"
echo "sha256=${S}" >> "$GITHUB_OUTPUT"
echo "- tarball: \`$(basename "${T}")\` sha256 \`${S}\`" >> "$GITHUB_STEP_SUMMARY"

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: npm-tarball
path: ${{ runner.temp }}/pack/*.tgz
if-no-files-found: error
retention-days: 30

publish:
name: publish(environment npm-publish の承認後)
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 15
environment: npm-publish
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: npm-tarball
path: ${{ runner.temp }}/pack

- name: tarball の同一性
env:
TARBALL: ${{ needs.verify.outputs.tarball }}
SHA256: ${{ needs.verify.outputs.sha256 }}
run: echo "${SHA256} ${RUNNER_TEMP}/pack/${TARBALL}" | sha256sum -c -

- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: ${{ env.PNPM_VERSION }}

- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: ${{ env.NODE_VERSION }}
# registry-url は渡さない(.npmrc にトークン行を作らない)

- name: OIDC 交換の確認(dry_run のときだけ・HTTP status だけを出す)
if: ${{ inputs.dry_run }}
env:
NAME: ${{ needs.verify.outputs.name }}
run: |
set -euo pipefail
ID=$(curl -sS -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=npm:registry.npmjs.org" \
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).value||""))')
[ -n "${ID}" ] || { echo "::error::GitHub から ID トークンを取れない(id-token 権限を確認)"; exit 1; }
echo "::add-mask::${ID}"
ESC=$(node -p 'encodeURIComponent(process.env.NAME).replace(/^%40/,"@")')
CODE=$(curl -sS -o /dev/null -w '%{http_code}' -X POST -H "Authorization: Bearer ${ID}" \
"https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/${ESC}")
echo "- OIDC exchange: HTTP ${CODE}" >> "$GITHUB_STEP_SUMMARY"
case "${CODE}" in 2??) ;; *) echo "::error::OIDC 交換が HTTP ${CODE}。npm の trusted publisher 登録(repo・publish.yml・npm-publish)を確認"; exit 1 ;; esac

- name: publish
working-directory: ${{ runner.temp }}/pack
env:
TARBALL: ${{ needs.verify.outputs.tarball }}
DIST_TAG: ${{ inputs.dist_tag }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
ARGS=(--access public --tag "${DIST_TAG}" --provenance --no-git-checks)
[ "${DRY_RUN}" = "true" ] && ARGS+=(--dry-run)
pnpm publish "./${TARBALL}" "${ARGS[@]}"

- name: 公開結果の確認
if: ${{ !inputs.dry_run }}
env:
NAME: ${{ needs.verify.outputs.name }}
VERSION: ${{ needs.verify.outputs.version }}
DIST_TAG: ${{ inputs.dist_tag }}
run: |
set -euo pipefail
# 版と dist-tag が一致するまで待つ(NayutaProducts#30)。404 の JSON・JSON でない出力では抜けない
[ -n "${VERSION}" ] || { echo "::error::VERSION が空(verify job の出力を確認)"; exit 1; }
JUDGE='
let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{
const want=process.env.VERSION, tag=process.env.DIST_TAG, final=process.env.FINAL==="1";
let p=null; try{p=JSON.parse(s)}catch(e){p=null}
const ok=!!p&&typeof p==="object"&&!Array.isArray(p)&&!p.error;
const ver=ok&&p.version===want;
const tagOk=ok&&(p["dist-tags"]||{})[tag]===want;
if(!final){process.exit(ver&&tagOk?0:1)}
const prov=ok&&!!(p.dist&&p.dist.attestations&&p.dist.attestations.provenance);
let t=null; try{t=JSON.parse(process.env.TP_JSON||"")}catch(e){t=null}
const tp=!!t&&typeof t==="object"&&!Array.isArray(t)&&!t.error&&typeof t.id==="string"&&t.id!=="";
console.log(`- registry: version=${ok?p.version:"(応答なし)"} dist-tags.${tag}=${tagOk} provenance=${prov} trustedPublisher=${tp}`);
const errs=[];
if(!ver)errs.push(`${want} が registry で見えない`);
if(!tagOk)errs.push(`dist-tag ${tag} が ${want} を指していない`);
if(!prov)errs.push("provenance が付いていない");
if(!tp)errs.push("trustedPublisher が付いていない");
if(errs.length){for(const m of errs)console.log(`::error::${m}`);console.log("::error::公開は済んだが照合に落ちた(取り消しはできない)。下流の波を止める");process.exit(1)}
})'
J=""
for i in $(seq 1 20); do
J=$(npm view "${NAME}@${VERSION}" --json 2>/dev/null || true)
if printf '%s' "${J}" | FINAL=0 node -e "${JUDGE}"; then break; fi
sleep 15
done
TP=$(npm view "${NAME}@${VERSION}" _npmUser.trustedPublisher --json 2>/dev/null || true)
printf '%s' "${J}" | FINAL=1 TP_JSON="${TP}" node -e "${JUDGE}" | tee -a "$GITHUB_STEP_SUMMARY"
67 changes: 30 additions & 37 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,49 @@
name: CI

# ブランチ非限定(Atlas 前例と同一)∴ branches キーを持たない。
# Runs on every branch (no branches key).
on:
push:
pull_request:

# Daedalus は依存を 1 件も持たない(dependencies キー自体が無い)∴ 単体 checkout で完結し、
# 兄弟リポジトリの checkout は不要である。デモ(index.html と demo/)は vite で作り、
# GitHub Pages への配備は deploy.yml が master への push で行う。ここではデモの build と
# Pages 用の成果物の形だけを検査する(verify:demo)。
permissions:
contents: read

# Daedalus has no dependencies (no dependencies key at all), so CI checks out this
# repository alone. The demo (index.html and demo/) is built with vite; deploy.yml
# publishes it to GitHub Pages on pushes to master. This job checks the demo build and
# the shape of the Pages artifact (verify:demo).
#
# deploy.yml runs only on pushes to master. This job installs the same way (pnpm
# 11.27.0, the CI pnpm settings and --frozen-lockfile) and includes the Node.js version
# deploy.yml builds with, so an install problem shows up on the pull request instead of
# after the merge. tests/ci-workflow.test.ts keeps the two in step.
jobs:
test:
name: Test
name: Test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22, 24]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 10
version: 11.27.0
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 22
node-version: ${{ matrix.node-version }}
cache: pnpm
# pnpm 11 reads allowBuilds, minimumReleaseAgeExclude and overrides only from
# pnpm-workspace.yaml, which is kept out of the repository root (see the comment in
# ci/pnpm-workspace.ci.yaml).
- name: Use CI pnpm settings
run: cp ci/pnpm-workspace.ci.yaml pnpm-workspace.yaml
- name: Install dependencies
run: pnpm install
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm run test
- name: Run typecheck
Expand All @@ -36,28 +54,3 @@ jobs:
run: pnpm run build
- name: Build and verify demo site
run: pnpm run verify:demo

# deploy.yml runs only on pushes to master, so its install path would otherwise be
# tried for the first time after merging. This job repeats the same steps (pnpm
# 11.27.0, Node.js 24, the CI pnpm settings and --frozen-lockfile) on every push
# and pull request, so a failing install shows up before the merge.
deploy-build:
name: Deploy build (same install as deploy.yml)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Install pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 11.27.0
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24
cache: pnpm
- name: Use CI pnpm settings
run: cp ci/pnpm-workspace.ci.yaml pnpm-workspace.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build and verify demo site
run: pnpm run verify:demo
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changelog

このプロジェクトの主な変更を記録します。版数は [Semantic Versioning](https://semver.org/) に従います。
Notable changes to this project are recorded here. Versions follow [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- 初版。npm に `@c4h/daedalus` として公開します。主な API は `fitWorldFile`(対応点から相似または affine のワールドファイルを当てはめ、採用の可否・warning・残差を返す)と、その入出力の型(`GcpPair`・`GeoBasis`・`FitOptions`・`FitResult`・`WorldFile` ほか)です。全 export は README の公開 API の節にあります。
- First release, published to npm as `@c4h/daedalus`. The main API is `fitWorldFile` (fits a similarity or affine world file to ground control points and reports whether it can be adopted, with warnings and residuals) and the types of its input and output (`GcpPair`, `GeoBasis`, `FitOptions`, `FitResult`, `WorldFile` and others). Every export is listed in the Public API section of the README.
- 対応していないことは README の「[非目標](./README.ja.md#非目標)」にあります。
- What is not supported is listed under "[Non-goals](./README.md#non-goals)" in the README.
Loading
Loading