Skip to content
181 changes: 181 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -603,3 +603,184 @@ jobs:
SKIP_BUILD: '1'
ALLOW_UNTAGGED: '1'
run: bash scripts/release-notes.sh "${GITHUB_REF_NAME}"

# This job's name is the status check context that branch protection or a
# ruleset would match on. Renaming it, including a capitalisation change,
# stops that check from reporting on every pull request, this rename's own
# included. The scope gate is per-step rather than one job-level `if:` so
# that a scoped-out pull request still reports a green check carrying the
# scope log that says which paths it looked at. A job-level `if:` would
# report `skipped` instead, which a required check accepts but which reads
# on the pull request as though the job never ran.
package:
name: package
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

# registry/dist is the package's payload and must not be committed, while
# a dist directory anywhere else under registry/ is build output of a kind
# nothing ships. The rule drawing that line lives in the ROOT .gitignore
# rather than beside the code, because npm applies a nested ignore file to
# the pack walk even for a path the manifest's "files" allowlist names,
# while the allowlist outranks the root file. That placement is exactly
# what hides the rule from every other check here: the root file cannot
# subtract from the tarball, so re-anchoring it to `registry/dist` leaves
# the smoke test and the manifest guard below both at exit 0 while build
# output under registry/src and registry/test silently stops being
# ignored. That is the regression 7d04a6c fixed, and this table is the
# only thing that holds it. Three things are asserted: a rule that stops
# covering the tree is as wrong as one that reaches past registry/'s own
# build output, and either pattern satisfies both of those from inside
# registry/.gitignore while emptying the package, so where the rule lives
# is read as well. Ungated, like the sweeps at the top of the daml job: it
# needs nothing but the checkout, so it reports in seconds and cannot be
# silenced by a later narrowing of the gate below.
- name: Verify the build-output ignore rule
run: |
set -euo pipefail
status=0
# check-ignore answers 0 when a rule matches and 1 when none does,
# and both are results rather than failures, so the status is held
# rather than left to -e, which would end the step at the first
# mismatch and hide every case after it. Anything above 1 means it
# could not answer at all: taking that for "no rule matched" would
# print the second list as passing while git was failing outright.
# None of these paths need to exist; the command matches patterns.
# --no-index because without it check-ignore consults the index and
# calls every TRACKED path unignored whatever the patterns say. That
# would make the registry/src/index.ts case below vacuous, since it is
# the one tracked path here: a rule that swallowed the service's own
# source would still read as a pass.
for path in registry/dist/index.js \
registry/src/dist/bundle.js \
registry/test/dist/bundle.js; do
rc=0
git check-ignore -q --no-index "$path" || rc=$?
case "$rc" in
0) echo "ok: ${path} is ignored" ;;
1) echo "${path} is not ignored: build output under registry/ is committable" >&2
status=1 ;;
*) echo "git check-ignore could not answer for ${path}: status ${rc}" >&2
status=1 ;;
esac
done
for path in registry/dist-backup/index.js \
registry/src/index.ts \
dist/index.js \
scripts/dist/index.js; do
rc=0
git check-ignore -q --no-index "$path" || rc=$?
case "$rc" in
1) echo "ok: ${path} is not ignored" ;;
0) echo "${path} is ignored: the rule reaches past registry/'s build output" >&2
status=1 ;;
*) echo "git check-ignore could not answer for ${path}: status ${rc}" >&2
status=1 ;;
esac
done
# Both loops read what the rule COVERS, and the same patterns cover
# the same tree from inside registry/.gitignore, where npm applies
# them to the pack walk as well: measured at 8 tarball entries rather
# than 24, registry/dist gone and the bin target with it, while all
# seven verdicts above stay green. The smoke test below is the only
# other thing that sees it, and only when the gate lets it run, so the
# file carrying the rule is read here too. -v prints the deciding
# source as `<file>:<line>:<pattern>`; the first field is the whole
# assertion.
rc=0
match="$(git check-ignore -v --no-index registry/dist/index.js)" || rc=$?
case "$rc" in
0) ignore_file="${match%%:*}"
if [ "$ignore_file" = ".gitignore" ]; then
echo "ok: the rule ignoring registry/dist lives in the root .gitignore"
else
echo "registry/dist is ignored by ${ignore_file} rather than the root .gitignore: a nested ignore file subtracts it from the npm pack walk" >&2
status=1
fi ;;
1) echo "no rule ignores registry/dist, so there is no placement to read" >&2
status=1 ;;
*) echo "git check-ignore could not answer for registry/dist/index.js: status ${rc}" >&2
status=1 ;;
esac
exit "$status"

- name: Scope
id: scope
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" != "pull_request" ]; then
echo "Not a pull request: verifying unconditionally."
echo "run=true" | tee -a "$GITHUB_OUTPUT"
exit 0
fi
# A blip on this fetch reds the job having verified nothing, so it is
# retried rather than taken at its word. The step above is ungated and
# sits there so that a red here finds it already run and reported.
for attempt in 1 2 3; do
git fetch --no-tags --prune origin \
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" && break
echo "git fetch attempt ${attempt} of 3 failed" >&2
[ "$attempt" -lt 3 ] || exit 1
sleep 5
done
changed="$(git -c core.quotePath=false diff --name-only --no-renames "origin/${BASE_REF}...HEAD")"
echo "Changed files:"
echo "$changed"
# The package's own inputs: the root manifest and lockfile by name,
# the registry ones and every packed source through the registry/
# prefix, and the two scripts the steps below run. LICENSE and
# README.md are packed too, whatever "files" says, and are left out
# on purpose: no check here or downstream asserts either, so gating
# on them would only lengthen the job. .gitignore is listed to keep
# this enumeration complete rather than for coverage, since the step
# above is ungated and reads that file on every event whether or not
# the gate names it, while the three steps below are blind to the
# root rule. .npmrc is the opposite case and this gate is the only
# thing holding it. It reconfigures npm for every step below rather
# than naming a file any of them read, which is what keeps it out of
# a list written by thinking about inputs: `omit=dev` on its own
# leaves prepare with no compiler, exit 127 before anything builds.
if grep -Eq '^(package\.json$|package-lock\.json$|registry/|scripts/check-registry-deps\.mjs$|scripts/registry-install-smoke\.sh$|\.gitignore$|\.npmrc$|\.github/workflows/ci\.yml$)' <<<"$changed"; then
echo "run=true" | tee -a "$GITHUB_OUTPUT"
else
echo "run=false" | tee -a "$GITHUB_OUTPUT"
fi

- uses: actions/setup-node@v4
if: steps.scope.outputs.run == 'true'
with:
node-version: '22'
cache: npm
cache-dependency-path: package-lock.json

# Ahead of the install it guards, for the reason the daml job places its
# own sweeps first: the script only reads four tracked JSON files, so it
# needs no node_modules and reports in a moment. npm ci is not a second
# reading of this. It reds only where the lockfile can no longer satisfy
# the range beside it, and then with npm's own "Invalid: lock file's
# express@4.22.2 does not satisfy express@5.2.1"; a widened range the
# lockfile still satisfies installs clean, and a disagreement between the
# two trees is invisible to it whatever the ranges say.
- name: Check the manifests agree
if: steps.scope.outputs.run == 'true'
run: npm run check:deps

# npm ci runs `prepare`, so this compiles registry/src against the ROOT
# dependency set, which is the set a consumer gets. The registry job
# compiles the same source against registry/node_modules; only this one
# would catch a type package that arrives there transitively and is
# declared nowhere.
- name: Install
if: steps.scope.outputs.run == 'true'
run: npm ci

- name: Install smoke test
if: steps.scope.outputs.run == 'true'
run: npm run smoke:registry
Loading