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
5 changes: 5 additions & 0 deletions .changeset/musl-build-image-dependabot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'stash': patch
---

The supply-chain skill names every Dependabot ecosystem the repository monitors, including the Go module and the Docker image the musl binaries are built in.
25 changes: 25 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,28 @@ updates:
- dependency-name: "*"
update-types:
- version-update:semver-major

# ── Docker ─────────────────────────────────────────────────────
# The Alpine image the musl binaries are built in. Only the `FROM` digest is
# read here: the `apk` pins in that Dockerfile are moved by hand, and
# musl-build-image.yml says when they stop resolving.
- package-ecosystem: docker
directory: /.github/docker/musl-build
schedule:
interval: weekly
day: monday
cooldown:
default-days: 7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: No test checks the cooldown or the schedule of the new docker Dependabot entry.

Impact: The skill now says Dependabot opens 7-day cooldown PRs for docker. If someone deletes cooldown: or changes the schedule here, no test fails. Dependabot can then propose a new base image digest on the day it is published.

Evidence: e2e/tests/supply-chain.e2e.test.ts checks the cooldown only for npm and github-actions. scripts/__tests__/musl-build-image.test.mjs checks only this entry's directory. Every entry in .github/dependabot.yml has default-days: 7 today, so a check on every entry passes on this branch.

Fix: In e2e/tests/supply-chain.e2e.test.ts, add this to the supply chain — automated dependency updates (Dependabot) block:

  it('every entry has a ≥ 3 day cooldown', () => {
    expect(db.updates.length).toBeGreaterThan(0)
    for (const entry of db.updates) {
      expect(
        entry.cooldown?.['default-days'] ?? 0,
        `${entry['package-ecosystem']} at ${entry.directory} has no cooldown of at least 3 days`,
      ).toBeGreaterThanOrEqual(3)
    }
  })

Then change "cooldown ≥ 3 days on npm/github-actions" on the "Test asserts" line in skills/stash-supply-chain-security/SKILL.md to "cooldown ≥ 3 days on every entry". To also keep the weekly schedule, extend the Dependabot test in musl-build-image.test.mjs:

    const entry = docker.find((update) => update.directory === `/${DOCKERFILE_DIR}`)
    expect(entry).toMatchObject({ schedule: { interval: 'weekly' } })

Found by 2 models: claude, codex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ee87669: every-entry cooldown test in supply-chain.e2e.test.ts, the skill's "Test asserts" line updated, and the docker entry's weekly schedule asserted in musl-build-image.test.mjs.

open-pull-requests-limit: 2
labels:
- dependencies
- supply-chain
commit-message:
prefix: "chore"
include: scope
ignore:
# A new Node.js major (node:24-alpine) is reviewed and applied manually,
# with the host legs' Node version, not by Dependabot.
- dependency-name: "*"
update-types:
- version-update:semver-major
28 changes: 28 additions & 0 deletions .github/docker/musl-build/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Builds the linux-x64-musl binaries of @cipherstash/auth and
# @cipherstash/protect-ffi. Alpine is a musl system, so its compiler links
# musl; the Ubuntu runner's compiler links glibc, and the result does not load
# on musl.
#
# Pinned because the output is published with provenance. Dependabot moves the
# digest; nothing moves the package versions, and Alpine drops a version from
# its index when it publishes the next one, so a pin can stop resolving at any
# time. musl-build-image.yml builds this file weekly so that is a failed check,
# not a failed release. Re-pin from the FROM image with
# `apk update >/dev/null && apk search --exact <packages>`.
FROM node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402

# build-base is a metapackage whose compiler and linker dependencies carry no
# version, so gcc, g++, binutils, musl-dev and make are pinned beside it.
RUN apk add --no-cache \
binutils=2.45.1-r1 \
build-base=0.5-r4 \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change before merge: build-base=0.5-r4 pins a metapackage, so the C compiler and the linker are still not pinned.

Impact: Two builds of one commit can still use different compilers. That is the problem #1042 describes. build-base depends on gcc, g++, binutils, make and libc-dev with no version. Alpine keeps only the newest build of each package in the 3.24 index. When Alpine publishes a new gcc build, build-base=0.5-r4 still resolves and installs it. The weekly musl-build-image.yml check stays green. The release then links the binary with a different gcc and ld, under the same provenance. 39 of the 46 packages that this apk add installs have no pin.

Evidence: In the pinned image (Alpine 3.24.2):

$ apk info -R build-base
build-base-0.5-r4 depends on:
binutils
file
gcc
g++
make
libc-dev
fortify-headers
patch

$ apk add --simulate --no-cache build-base=0.5-r4 cmake=4.2.3-r0 ... rustup=1.29.0-r0
( 3/46) Installing binutils (2.45.1-r1)
(13/46) Installing gcc (15.2.0-r5)
(15/46) Installing musl-dev (1.2.6-r2)
(16/46) Installing g++ (15.2.0-r5)
(17/46) Installing make (4.4.1-r4)

Fix: Also pin the packages that compile and link the binary: gcc, g++, binutils, musl-dev and make. Add the same names to TOOLCHAIN in scripts/__tests__/musl-build-image.test.mjs, so the test fails if a pin is removed. This list resolves in the pinned image (apk add --simulate prints OK):

RUN apk add --no-cache \
    binutils=2.45.1-r1 \
    build-base=0.5-r4 \
    cmake=4.2.3-r0 \
    curl=8.22.0-r0 \
    g++=15.2.0-r5 \
    gcc=15.2.0-r5 \
    git=2.54.0-r0 \
    linux-headers=7.0.0-r1 \
    make=4.4.1-r4 \
    musl-dev=1.2.6-r2 \
    perl=5.42.2-r1 \
    rustup=1.29.0-r0

Each new pin can stop resolving, like the existing ones. gcc and binutils change rarely inside one Alpine stable branch. If you do not want more pins, change the Dockerfile comment and the PR description so they do not say the compiler is pinned. Then keep #1042 open for the compiler.

Found by 1 model: claude

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ee87669: binutils, g++, gcc, make and musl-dev pinned at the versions you listed (image rebuilt locally, 46 packages, OK), and all five added to TOOLCHAIN.

cmake=4.2.3-r0 \
curl=8.22.0-r0 \
g++=15.2.0-r5 \
gcc=15.2.0-r5 \
git=2.54.0-r0 \
linux-headers=7.0.0-r1 \
make=4.4.1-r4 \
musl-dev=1.2.6-r2 \
perl=5.42.2-r1 \
rustup=1.29.0-r0
17 changes: 9 additions & 8 deletions .github/workflows/_build-auth-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,28 +118,29 @@ jobs:
# suite's 0.44.0 has that fault. Ubuntu's musl-gcc cannot link a Rust
# shared library (it has no musl libgcc_s), and the musl.cc toolchain
# that _build-ffi-artifacts.yml downloads timed out from GitHub's runners
# on 2 October 2026. The image is pinned by digest because its output is
# published with provenance. `-crt-static` keeps the binary linked
# against musl at load time, which a Node.js native module needs, and
# which the C library check below reads. The same napi flags as the host
# build, for the same reasons.
# on 2 October 2026. The image is .github/docker/musl-build/Dockerfile,
# base image and packages pinned, because its output is published with
# provenance. `-crt-static` keeps the binary linked against musl at load
# time, which a Node.js native module needs, and which the C library
# check below reads. The same napi flags as the host build, for the same
# reasons.
- name: Build the native binding in Alpine (linux-x64-musl)
if: ${{ matrix.platform == 'linux-x64-musl' }}
env:
TARGET: ${{ matrix.target }}
ALPINE_NODE_IMAGE: node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402
MUSL_BUILD_IMAGE: cipherstash/musl-build:local
run: |
set -euo pipefail
rust_version=$(mise current rust)
pnpm_spec=$(node -p "require('./package.json').packageManager")
docker build --pull -t "$MUSL_BUILD_IMAGE" .github/docker/musl-build
docker run --rm \
-v "$GITHUB_WORKSPACE:/build" -w /build \
-e TARGET -e RUST_VERSION="$rust_version" -e PNPM_SPEC="$pnpm_spec" \
-e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
-e RUSTFLAGS="-C target-feature=-crt-static" \
"$ALPINE_NODE_IMAGE" sh -euc '
"$MUSL_BUILD_IMAGE" sh -euc '
trap "chown -R \"\$HOST_UID:\$HOST_GID\" /build" EXIT
apk add --no-cache build-base cmake perl linux-headers git curl rustup
rustup-init -y --profile minimal --default-toolchain "$RUST_VERSION" --target "$TARGET"
. "$HOME/.cargo/env"
corepack enable
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/_build-ffi-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -235,20 +235,20 @@ jobs:
PLATFORM: ${{ matrix.cfg.platform }}
BUILD_SCRIPT: ${{ matrix.cfg.script }}
BUILD_LOG: ${{ matrix.cfg.log }}
ALPINE_NODE_IMAGE: node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402
MUSL_BUILD_IMAGE: cipherstash/musl-build:local
run: |
set -euo pipefail
rust_version=$(rustc --version | cut -d' ' -f2)
pnpm_spec=$(node -p "require('./package.json').packageManager")
docker build --pull -t "$MUSL_BUILD_IMAGE" .github/docker/musl-build
docker run --rm \
-v "$GITHUB_WORKSPACE:/build" -w /build \
-e CARGO_BUILD_TARGET -e PLATFORM -e BUILD_SCRIPT -e BUILD_LOG \
-e RUST_VERSION="$rust_version" -e PNPM_SPEC="$pnpm_spec" \
-e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
-e RUSTFLAGS="-C target-feature=-crt-static" \
"$ALPINE_NODE_IMAGE" sh -euc '
"$MUSL_BUILD_IMAGE" sh -euc '
trap "chown -R \"\$HOST_UID:\$HOST_GID\" /build" EXIT
apk add --no-cache build-base cmake perl linux-headers git curl rustup
rustup-init -y --profile minimal --default-toolchain "$RUST_VERSION" --target "$CARGO_BUILD_TARGET"
. "$HOME/.cargo/env"
corepack enable
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/auth-preflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ jobs:
with:
ref: ${{ inputs.ref }}
persist-credentials: false
sparse-checkout: scripts
# The musl smoke test reads the Dockerfile's FROM line.
sparse-checkout: |
scripts
.github/docker/musl-build

- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
Expand Down Expand Up @@ -131,12 +134,14 @@ jobs:

# The host step above installs only linux-x64-gnu, the runner's own
# platform. The musl binary loads only where musl is the C library, so it
# is installed and loaded inside Alpine, from the image the build uses.
# is installed and loaded inside Alpine, on the base image the build
# image is made from: plain Node.js on musl, as a user's container is.
# Read from the Dockerfile so there is one digest for Dependabot to move.
- name: Smoke-test the musl artifact inside Alpine
env:
ALPINE_NODE_IMAGE: node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402
run: |
set -euo pipefail
ALPINE_NODE_IMAGE=$(sed -n 's/^FROM //p' .github/docker/musl-build/Dockerfile)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix before merge: The smoke job's checkout does not include .github/docker/musl-build, so this sed cannot read the Dockerfile.

Impact: Every dispatched auth-preflight run fails in this step, before Docker starts. The preflight never installs or loads the musl tarball. So the preflight does not check that tarball before a release.

Evidence: The actions/checkout step at line 50 sets sparse-checkout: scripts. In cone mode, Git then checks out only scripts/ and the files at the repository root. The checkout does not include .github/. I reproduced the checkout with git sparse-checkout set --cone scripts. Then I ran this line, and it printed:

sed: can't read .github/docker/musl-build/Dockerfile: No such file or directory

set -euo pipefail then stops the step with exit code 2. The test smoke-test the musl artifacts on the image the Dockerfile starts from checks the sed text. It does not check the checkout, so the test passes.

Fix: Add the Dockerfile directory to the sparse checkout at line 50:

          sparse-checkout: |
            scripts
            .github/docker/musl-build

With this list, git sparse-checkout set --cone checks out the Dockerfile, and the sed prints the pinned image.

To stop the same mistake later, add a check to scripts/__tests__/musl-build-image.test.mjs. Find the loop in the test smoke-test the musl artifacts on the image the Dockerfile starts from. Put this code at the end of the loop body:

      const job = Object.values(wf.jobs).find((j) =>
        (j?.steps ?? []).some((s) => String(s?.run ?? '').includes(DOCKERFILE)),
      )
      const checkout = job.steps.find((s) =>
        String(s?.uses ?? '').startsWith('actions/checkout'),
      )
      const sparse = String(checkout?.with?.['sparse-checkout'] ?? '')
        .split('\n')
        .map((path) => path.trim())
        .filter(Boolean)
      if (sparse.length > 0) expect(sparse).toContain(DOCKERFILE_DIR)

The code finds the job that has a step whose run text contains DOCKERFILE. Then it finds the actions/checkout step in that job. If that step sets sparse-checkout, the code expects DOCKERFILE_DIR in the list of paths.

The code uses three names. Check that the test defines them with these values:

  • wf is the parsed workflow file for the current pass of the loop.
  • DOCKERFILE is the Dockerfile path that the step's run text contains: .github/docker/musl-build/Dockerfile.
  • DOCKERFILE_DIR is exactly .github/docker/musl-build, with no leading slash. With a leading slash, toContain fails.

If the test uses other names for these values, change the code to use those names.

.github/workflows/ffi-preflight.yml has the same problem at line 161.

Found by 1 model: codex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ee87669: .github/docker/musl-build is in the sparse checkout of both preflights, and the test now reads each smoke job's checkout and demands it.

test -n "$ALPINE_NODE_IMAGE"
wrapper=$(basename "$(ls "$GITHUB_WORKSPACE"/auth-dist/cipherstash-auth-[0-9]*.tgz)")
musl=$(basename "$(ls "$GITHUB_WORKSPACE"/auth-dist/cipherstash-auth-linux-x64-musl-*.tgz)")
docker run --rm -v "$GITHUB_WORKSPACE/auth-dist:/dist:ro" \
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/ffi-preflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ jobs:
with:
ref: ${{ inputs.ref }}
persist-credentials: false
sparse-checkout: scripts
# The musl smoke test reads the Dockerfile's FROM line.
sparse-checkout: |
scripts
.github/docker/musl-build

- uses: actions/download-artifact@v4
with:
Expand Down Expand Up @@ -152,12 +155,14 @@ jobs:

# The host steps above install only linux-x64-gnu, the runner's own
# platform. The musl binary loads only where musl is the C library, so it
# is installed and loaded inside Alpine, from the image the build uses.
# is installed and loaded inside Alpine, on the base image the build
# image is made from: plain Node.js on musl, as a user's container is.
# Read from the Dockerfile so there is one digest for Dependabot to move.
- name: Smoke-test the musl artifact inside Alpine
env:
ALPINE_NODE_IMAGE: node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402
run: |
set -euo pipefail
ALPINE_NODE_IMAGE=$(sed -n 's/^FROM //p' .github/docker/musl-build/Dockerfile)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix before merge: The smoke job's checkout does not include .github/docker/musl-build, so this sed cannot read the Dockerfile.

Impact: Every dispatched ffi-preflight run fails in this step, before Docker starts. The preflight never installs or loads the musl tarball. AGENTS.md says to dispatch this workflow before you merge a release that moves an FFI version. That check no longer works.

Evidence: The actions/checkout step at line 52 sets sparse-checkout: scripts. In cone mode, Git then checks out only scripts/ and the files at the repository root. The checkout does not include .github/. With that checkout, this line prints:

sed: can't read .github/docker/musl-build/Dockerfile: No such file or directory

set -euo pipefail then stops the step with exit code 2.

Fix: Add the Dockerfile directory to the sparse checkout at line 52:

          sparse-checkout: |
            scripts
            .github/docker/musl-build

.github/workflows/auth-preflight.yml needs the same change at line 50.

To stop the same mistake later, add a check to scripts/__tests__/musl-build-image.test.mjs. Find the loop in the test smoke-test the musl artifacts on the image the Dockerfile starts from. Put this code at the end of the loop body:

      const job = Object.values(wf.jobs).find((j) =>
        (j?.steps ?? []).some((s) => String(s?.run ?? '').includes(DOCKERFILE)),
      )
      const checkout = job.steps.find((s) =>
        String(s?.uses ?? '').startsWith('actions/checkout'),
      )
      const sparse = String(checkout?.with?.['sparse-checkout'] ?? '')
        .split('\n')
        .map((path) => path.trim())
        .filter(Boolean)
      if (sparse.length > 0) expect(sparse).toContain(DOCKERFILE_DIR)

The code finds the job that has a step whose run text contains DOCKERFILE. Then it finds the actions/checkout step in that job. If that step sets sparse-checkout, the code expects DOCKERFILE_DIR in the list of paths.

The code uses three names. Check that the test defines them with these values:

  • wf is the parsed workflow file for the current pass of the loop.
  • DOCKERFILE is the Dockerfile path that the step's run text contains: .github/docker/musl-build/Dockerfile.
  • DOCKERFILE_DIR is exactly .github/docker/musl-build, with no leading slash. With a leading slash, toContain fails.

If the test uses other names for these values, change the code to use those names.

Found by 1 model: codex

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ee87669, with auth-preflight.

test -n "$ALPINE_NODE_IMAGE"
wrapper=$(basename "$(ls "$GITHUB_WORKSPACE"/ffi-dist/cipherstash-protect-ffi-[0-9]*.tgz)")
musl=$(basename "$(ls "$GITHUB_WORKSPACE"/ffi-dist/cipherstash-protect-ffi-linux-x64-musl-*.tgz)")
docker run --rm -v "$GITHUB_WORKSPACE/ffi-dist:/dist:ro" \
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/musl-build-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: musl build image

# The Dockerfile pins every apk package to an exact version, and Alpine drops a
# version from its index when it publishes the next one. Nothing else builds
# the image between releases, so without this a pin that stopped resolving
# would first be seen by a release.
on:
push:
branches:
- main
paths:
- ".github/docker/musl-build/**"
- ".github/workflows/musl-build-image.yml"
pull_request:
paths:
- ".github/docker/musl-build/**"
- ".github/workflows/musl-build-image.yml"
schedule:
- cron: "0 7 * * 1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix in a follow-up: When the weekly run fails, GitHub notifies only one person.

Impact: GitHub sends the failure notice for a schedule run only to the user who created the workflow, or to the last user who changed its cron line. No issue opens, and no one else gets a message. If that person misses the notice, the team first sees the failure in a release build. This workflow exists to stop that outcome.

Evidence: GitHub documentation, "Notifications for workflow runs": "Notifications for scheduled workflows are sent to the user who initially created the workflow. If a different user updates the cron syntax in the workflow file, subsequent notifications will be sent to that user instead." No scheduled workflow in .github/workflows/ opens an issue or sends a message today. One shared fix can serve all of them.

Fix: Open an issue when the scheduled run fails. Give issues: write to this job only:

jobs:
  build:
    permissions:
      contents: read
      issues: write
    steps:
      # ... existing steps ...
      - name: Open an issue when the weekly build fails
        if: ${{ failure() && github.event_name == 'schedule' }}
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh issue create --repo "$GITHUB_REPOSITORY" \
            --title "musl build image no longer builds" \
            --body "Run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID. Re-pin with the command in .github/docker/musl-build/Dockerfile."

Found by 1 model: claude

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken now rather than later, in ee87669: the job has issues: write and opens an issue on a failed scheduled run; the test asserts the step's if.

workflow_dispatch:

permissions:
contents: read

jobs:
build:
name: Build the Alpine musl image
runs-on: ubuntu-latest
timeout-minutes: 15
# `issues: write` for the last step alone; see there.
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
# `--pull` fetches the digest the Dockerfile names rather than a cached
# tag, so the check builds what a release would.
- name: Build the image
run: docker build --pull -t cipherstash/musl-build:check .github/docker/musl-build
# The build steps rely on these being present; a Dockerfile that builds
# but lost one would fail minutes into a release build instead.
- name: Check the tools the build steps use
run: |
set -euo pipefail
docker run --rm cipherstash/musl-build:check sh -euc '
echo "Alpine $(cat /etc/alpine-release)"
for tool in cc c++ cmake perl make curl git rustup-init node; do
command -v "$tool" >/dev/null || { echo "missing: $tool" >&2; exit 1; }
done
'
# GitHub mails a scheduled run's failure to one person: whoever last
# edited the cron line. An issue is what the rest of the team sees.
- name: Open an issue when the weekly build fails
if: ${{ failure() && github.event_name == 'schedule' }}
env:
GH_TOKEN: ${{ github.token }}
run: |
gh issue create --repo "$GITHUB_REPOSITORY" \
--title "The musl build image no longer builds" \
--label "github-actions" \
--body "The weekly build of .github/docker/musl-build/Dockerfile failed: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID. An apk pin has most likely stopped resolving; re-pin with the command in the Dockerfile's comment."
30 changes: 30 additions & 0 deletions e2e/tests/supply-chain.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ const MANIFEST_BY_ECOSYSTEM: Record<string, string> = {
uv: 'pyproject.toml',
mix: 'mix.exs',
pub: 'pubspec.yaml',
docker: 'Dockerfile',
// github-actions is special-cased: Dependabot requires `directory: /` and
// discovers .github/workflows itself.
'github-actions': '.github/workflows',
Expand Down Expand Up @@ -722,6 +723,35 @@ describe('supply chain — automated dependency updates (Dependabot)', () => {
expect(gha?.cooldown?.['default-days']).toBeGreaterThanOrEqual(3)
})

it('every entry has a ≥ 3 day cooldown', () => {
// The two above are the named controls; this is the one that catches an
// entry added later without one, which would propose a dependency the
// day it is published.
expect(db.updates.length).toBeGreaterThan(0)
for (const entry of db.updates) {
expect(
entry.cooldown?.['default-days'] ?? 0,
`${entry['package-ecosystem']} at ${entry.directory} has no cooldown of at least 3 days`,
).toBeGreaterThanOrEqual(3)
}
})

it('the skill names every ecosystem Dependabot monitors', () => {
// The sentence is what a customer's agent reads; it omitted gomod for
// months with nothing to say so.
const skill = read('skills/stash-supply-chain-security/SKILL.md')
const sentence = skill
.split('\n')
.find((line) => line.startsWith('Dependabot opens grouped'))
expect(sentence).toBeDefined()
const ecosystems = new Set(db.updates.map((u) => u['package-ecosystem']))
for (const ecosystem of ecosystems) {
expect(sentence, `the skill does not name \`${ecosystem}\``).toContain(
`\`${ecosystem}\``,
)
}
})

it('every entry ignores majors, so none configures a major cooldown window', () => {
// One relationship, asserted from both ends, because either end alone
// passes on the drift that matters.
Expand Down
22 changes: 10 additions & 12 deletions scripts/__tests__/auth-build-artifacts.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,18 @@ describe('_build-auth-artifacts.yml', () => {
for (const build of builds) expect(check).toBeGreaterThan(build)
})

it('builds the musl binding inside Alpine, from an image pinned by digest', () => {
it('builds the musl binding inside Alpine, from the pinned Dockerfile', () => {
// Built on the Ubuntu runner, the musl binary linked glibc and failed to
// load on musl. Alpine is a musl system, so its compiler links musl.
// load on musl. Alpine is a musl system, so its compiler links musl. The
// image's pins, and that this is its one home, are musl-build-image.test.mjs's.
const steps = binaries?.steps ?? []
const musl = steps.filter((step) =>
String(step?.if ?? '').includes("== 'linux-x64-musl'"),
)
const run = musl.map((step) => String(step?.run ?? '')).join('\n')
const env = Object.assign({}, ...musl.map((step) => step?.env ?? {}))
expect(env.ALPINE_NODE_IMAGE).toMatch(/-alpine@sha256:[0-9a-f]{64}$/)
// The pinned image is the one that runs, not a mutable tag.
expect(run).toMatch(/docker run[\s\S]*"\$ALPINE_NODE_IMAGE"/)
expect(run).not.toMatch(/\bnode:\d+-alpine(?!@)/)
expect(run).toMatch(
/docker build --pull -t "\$MUSL_BUILD_IMAGE" \.github\/docker\/musl-build\n[\s\S]*docker run[\s\S]*"\$MUSL_BUILD_IMAGE"/,
)
expect(run).toContain('RUSTFLAGS="-C target-feature=-crt-static"')
// The container loads the binary it built, on musl.
expect(run).toMatch(
Expand Down Expand Up @@ -162,12 +161,11 @@ describe('_build-auth-artifacts.yml', () => {
)
expect(alpine).toBeDefined()
expect(String(alpine.run)).toContain('linux-x64-musl')
expect(String(alpine.run)).toMatch(/docker run[\s\S]*"\$ALPINE_NODE_IMAGE"/)
// The same image as the build, so the load test matches the build.
const build = (binaries?.steps ?? []).find(
(step) => step?.env?.ALPINE_NODE_IMAGE,
// On the image the build image starts from, read from the Dockerfile.
expect(String(alpine.run)).toContain(
"ALPINE_NODE_IMAGE=$(sed -n 's/^FROM //p'",
)
expect(alpine.env?.ALPINE_NODE_IMAGE).toBe(build?.env?.ALPINE_NODE_IMAGE)
expect(String(alpine.run)).toMatch(/docker run[\s\S]*"\$ALPINE_NODE_IMAGE"/)
})

it('packs the wrapper with pnpm, which rewrites its workspace peers', () => {
Expand Down
Loading
Loading