Skip to content

Release 1.1.0 - #12

Merged
en0f merged 7 commits into
stablefrom
dev
Nov 9, 2026
Merged

en0f merged 7 commits into
stablefrom
dev

Conversation

@pjhartlieb

@pjhartlieb pjhartlieb commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Promotes dev to stable as v1.1.0 (version is in pyproject.toml).

stable has nothing dev lacks, so no merge-back afterwards.

Merge with a merge commit. On merge, release.yml waits twice at the release environment. Each approval needs a bbot-leads member other than whoever merged.

pjhartlieb and others added 5 commits October 30, 2026 15:43
…v1.0.0

Sync dev with stable after the v1.0.0 release
Adds workflow_call so a repository's dev ruleset can require dev-security / dependency-review and dev-security / workflow-lint, the policy rule 3 checks, from one shared copy. README documents the caller. Version 1.1.0: a new way to call, nothing a caller must change.
…usable

feat: let other repositories call dev-security.yml
@pjhartlieb
pjhartlieb requested a review from a team as a code owner November 6, 2026 17:43

@en0f en0f left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed at c3bc4cb.

🔴 No run shows dependency review working when dev-security.yml is called from another repository

The check names are fine: they come from the job ids, the same way this repo's own CI already reports python / lint. What hasn't been shown is dependency-review-action finding the caller's pull request base and head when it runs inside a called workflow, and neither this PR nor #11 has a caller run. Once v1.1.0 is tagged, the README block becomes what every repo puts in its dev ruleset, and if dependency review errors there, those rulesets block on a required check that never passes. Please create a new public repo holding only the README caller workflow and open a pull request in it, with the caller pinned to c3bc4cb (or a later head whose dev-security.yml is unchanged). Pin by full SHA, not a branch, and drop the # v1.1.0 comment since that tag doesn't exist yet. Link the run here: it should show dev-security / dependency-review and dev-security / workflow-lint passing, with the dependency-review log showing the comparison rather than a missing base/head error. A fresh repo matters because workflow-lint lints every workflow in the caller repo, so an existing repo's own findings would turn it red for unrelated reasons.

🟢 workflow_call added without widening any permission

dev-security.yml keeps permissions: {} at the top and contents: read on each job, so it asks callers for nothing beyond the contents: read the README grants, and keeping the pull_request trigger means this repo's required checks keep their names.

🟢 Version and pytest bump are consistent, and the lock matches PyPI

1.1.0 is a minor bump for an additive trigger and matches in pyproject.toml and uv.lock. pytest 9.0.3 is pinned in all three places, and both the sdist and wheel hashes in uv.lock match what PyPI publishes.

Reproduce this review
# save the script below as verify-12.sh and the patch as control-12.patch, then:
bash verify-12.sh blacklanternsecurity/release-workflows c3bc4cb3c30af3b71f7cebf02225cb0db810a564
# expected: SUMMARY 11 PASS, 1 FAIL, 0 SKIP, 0 BROKEN (V12 is a README wording note, not a blocker)
# control:  bash verify-12.sh blacklanternsecurity/release-workflows c3bc4cb3c30af3b71f7cebf02225cb0db810a564 "$PWD/control-12.patch"
# expected: SUMMARY 7 PASS, 5 FAIL, 0 SKIP, 0 BROKEN (V01 V06 V07 V09 fail, plus V12)
#!/usr/bin/env bash
# verify-12.sh: reproducible checks for blacklanternsecurity/release-workflows#12
# Usage: bash verify-12.sh <owner/repo> <sha> [<control-patch>]
# Changes nothing outside a mktemp directory and nothing on GitHub (PR-VF-02).
#
# CONTROL: patch control-12.patch
# CONTROL-EXPECT: V01=FAIL V06=FAIL V07=FAIL V09=FAIL
set -uo pipefail

REPO="${1:?owner/repo}"; SHA="${2:?sha}"; PATCH="${3:-}"
BASE_SHA=c53c202eedeb26a86088936363e6cb06ce99ac15
PASS=0; FAIL=0; SKIP=0; BROKEN=0
pass()   { echo "PASS $1 $2"; PASS=$((PASS+1)); }
fail()   { echo "FAIL $1 $2 -- $3"; FAIL=$((FAIL+1)); }
skip()   { echo "SKIP $1 $2 -- $3"; SKIP=$((SKIP+1)); }
broken() { echo "BROKEN $1 $2 -- $3"; BROKEN=$((BROKEN+1)); }
summary() { echo "SUMMARY $PASS PASS, $FAIL FAIL, $SKIP SKIP, $BROKEN BROKEN"; [ "$FAIL" -eq 0 ] && [ "$BROKEN" -eq 0 ]; }

echo "script_sha256 $(shasum -a 256 "$0" | cut -d' ' -f1)"
echo "git $(git --version | cut -d' ' -f3)"
echo "python3 $(python3 --version | cut -d' ' -f2)"
echo "curl $(curl --version | head -1 | cut -d' ' -f2)"

WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 GIT_TERMINAL_PROMPT=0
GIT=(git -c credential.helper= -c 'credential.helper=!gh auth git-credential')
"${GIT[@]}" clone --quiet --filter=blob:none --no-checkout "https://github.com/$REPO.git" "$WORK/src" || { broken V00 "clone" "clone failed"; summary; exit 1; }
cd "$WORK/src"
"${GIT[@]}" cat-file -e "$SHA^{commit}" 2>/dev/null || "${GIT[@]}" fetch --quiet origin "+refs/pull/*/head:refs/remotes/pr/*" || true
"${GIT[@]}" checkout --quiet --detach "$SHA" || { broken V00 "checkout $SHA" "checkout failed"; summary; exit 1; }
[ "$(git rev-parse HEAD)" = "$SHA" ] || { broken V00 "HEAD is $SHA" "HEAD mismatch"; summary; exit 1; }
if [ -n "$PATCH" ]; then git apply "$PATCH" || { broken V00 "apply control patch" "git apply failed"; summary; exit 1; }; fi

# ---- checks: each line states the desired property --------------------------
WF=.github/workflows/dev-security.yml
# Parse the workflow with Python's stdlib only: the trigger keys and per-job permissions.
# (`on:` is read as text so YAML 1.1's on->True quirk does not matter.)
TRIGGERS=$(awk '/^on:/{s=1;next} /^[^ ]/{s=0} s && /^  [a-z_]+:/{gsub(/[ :]/,"");print}' "$WF" | sort | tr '\n' ' ')
JOBS=$(awk '/^jobs:/{s=1;next} /^[^ ]/{s=0} s && /^  [a-z][a-z0-9_-]*:$/{gsub(/[ :]/,"");print}' "$WF" | sort | tr '\n' ' ')

case " $TRIGGERS " in *" workflow_call "*) pass V01 "dev-security.yml can be called from other repositories (workflow_call)";;
  *) fail V01 "dev-security.yml can be called from other repositories (workflow_call)" "triggers: $TRIGGERS";; esac
case " $TRIGGERS " in *" pull_request "*) pass V02 "dev-security.yml still runs on this repository's own pull requests";;
  *) fail V02 "dev-security.yml still runs on this repository's own pull requests" "triggers: $TRIGGERS";; esac
if [ "$JOBS" = "dependency-review workflow-lint " ]; then
  pass V03 "job ids give the README's check names dev-security / dependency-review and dev-security / workflow-lint"
else fail V03 "job ids give the README's check names" "jobs: $JOBS"; fi

# Top-level permissions {} and every job asks for contents: read only, so the README caller's grant suffices.
PERMS_OK=1
grep -qE '^permissions: \{\}$' "$WF" || PERMS_OK=0
[ "$(grep -cE '^    permissions:$' "$WF")" -eq 2 ] || PERMS_OK=0
[ "$(grep -cE '^      contents: read$' "$WF")" -eq 2 ] || PERMS_OK=0
grep -E '^      [a-z-]+: (read|write)$' "$WF" | grep -vq 'contents: read' && PERMS_OK=0
if [ "$PERMS_OK" = 1 ]; then pass V04 "dev-security.yml asks for nothing beyond contents: read (top level {}, contents: read per job)"
else fail V04 "dev-security.yml asks for nothing beyond contents: read (top level {}, contents: read per job)" "permissions differ"; fi

README_CALLER=$(awk '/^Call `dev-security.yml`/{s=1} s' README.md | awk '/^```yaml/{f=1;next} /^```/{if(f)exit} f')
if echo "$README_CALLER" | grep -qE '^  pull_request:$' && echo "$README_CALLER" | grep -qE '^  contents: read$' \
   && echo "$README_CALLER" | grep -qE '^  dev-security:$' \
   && echo "$README_CALLER" | grep -qE 'uses: blacklanternsecurity/release-workflows/\.github/workflows/dev-security\.yml@<full sha of the tag> # v1\.1\.0$'; then
  pass V05 "README caller runs on pull_request, grants contents: read, names the job dev-security and pins v1.1.0 by SHA"
else fail V05 "README caller example is complete" "missing trigger, permission, job name or pin"; fi

PY_VER=$(sed -nE 's/^version = "([^"]+)"$/\1/p' pyproject.toml | head -1)
LOCK_VER=$(awk '/^name = "release-workflows"$/{getline; print}' uv.lock | sed -nE 's/^version = "([^"]+)"$/\1/p')
if [ "$PY_VER" = "1.1.0" ] && [ "$LOCK_VER" = "1.1.0" ] && echo "$PY_VER" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
  pass V06 "version is MAJOR.MINOR.PATCH 1.1.0 in pyproject.toml and uv.lock"
else fail V06 "version is MAJOR.MINOR.PATCH 1.1.0 in pyproject.toml and uv.lock" "pyproject=$PY_VER lock=$LOCK_VER"; fi

LOCK_PYTEST=$(awk '/^name = "pytest"$/{getline; print}' uv.lock | sed -nE 's/^version = "([^"]+)"$/\1/p')
if grep -q '"pytest==9.0.3"' pyproject.toml && [ "$LOCK_PYTEST" = "9.0.3" ] && grep -q '{ name = "pytest", specifier = "==9.0.3" }' uv.lock; then
  pass V07 "pytest is 9.0.3 in pyproject.toml and both places in uv.lock"
else fail V07 "pytest is 9.0.3 in pyproject.toml and uv.lock" "lock=$LOCK_PYTEST"; fi

# uv.lock's pytest 9.0.3 hashes match what PyPI publishes (anonymous HTTPS).
if curl -fsS https://pypi.org/pypi/pytest/9.0.3/json -o "$WORK/pytest.json"; then
  MISMATCH=$(python3 - "$WORK/pytest.json" uv.lock <<'PY'
import json, re, sys
pub = {u["filename"]: u["digests"]["sha256"] for u in json.load(open(sys.argv[1]))["urls"]}
lock = open(sys.argv[2]).read()
seen = 0
for name, h in re.findall(r'url = "https://files\.pythonhosted\.org/[^"]*/(pytest-9\.0\.3[^"/]*)", hash = "sha256:([0-9a-f]{64})"', lock):
    seen += 1
    if pub.get(name) != h:
        print(f"{name}: lock {h} pypi {pub.get(name)}")
if seen != 2:
    print(f"expected 2 pytest-9.0.3 artifacts in uv.lock, found {seen}")
PY
)
  if [ -z "$MISMATCH" ]; then pass V08 "uv.lock pytest 9.0.3 sdist and wheel hashes match PyPI"
  else fail V08 "uv.lock pytest 9.0.3 hashes match PyPI" "$MISMATCH"; fi
else skip V08 "uv.lock pytest 9.0.3 hashes match PyPI" "PyPI unreachable"; fi

# Every third-party action in dev-security.yml is pinned by a full commit SHA with a version comment.
UNPINNED=$(grep -E '^\s*-?\s*uses: ' "$WF" | grep -vE '@[0-9a-f]{40} # v[0-9]' || true)
if [ -z "$UNPINNED" ]; then pass V09 "every action in dev-security.yml is pinned by full SHA with a version comment"
else fail V09 "every action in dev-security.yml is pinned by full SHA" "$(echo "$UNPINNED" | tr -s ' ' | head -3)"; fi

# stable (the base, tag v1.0.0) is an ancestor of the head, so nothing on stable is lost and no merge-back is needed.
"${GIT[@]}" fetch --quiet origin "$BASE_SHA" 2>/dev/null || true
if git merge-base --is-ancestor "$BASE_SHA" HEAD 2>/dev/null; then pass V10 "stable ($BASE_SHA) is an ancestor of the head"
else fail V10 "stable is an ancestor of the head" "not an ancestor"; fi
# (C6) No job-level name: in dev-security.yml, so the reported check names are the job ids V03 compares.
if /usr/bin/grep -qE '^    name:' "$WF"; then fail V11 "dev-security.yml jobs have no name: that would override the check name" "$(/usr/bin/grep -nE '^    name:' "$WF" | head -1)"
else pass V11 "dev-security.yml jobs have no name: that would override the check name"; fi

# (C11) README's "map it by name as above" follows a yaml example that maps secrets.
NEAREST=$(awk '/^```yaml/{b="";f=1;next} f&&/^```/{f=0;last=b;next} f{b=b $0 "\n"} /name as above/{ if (last ~ /secrets:/) print "yes"; else print "no"; exit}' README.md)
case "$NEAREST" in yes) pass V12 "the example 'as above' refers to maps secrets by name";;
  no) fail V12 "the example 'as above' refers to maps secrets by name" "the nearest block above README's 'map it by name as above' is the dev-security caller, which maps no secrets";;
  *) skip V12 "the example 'as above' refers to maps secrets by name" "sentence not found";; esac
# ------------------------------------------------------------------------------
summary
diff --git a/.github/workflows/dev-security.yml b/.github/workflows/dev-security.yml
index 259723d..e9eb863 100644
--- a/.github/workflows/dev-security.yml
+++ b/.github/workflows/dev-security.yml
@@ -3,7 +3,6 @@
 name: dev-security
 on:
   pull_request:
-  workflow_call:
 
 permissions: {}
 
@@ -22,7 +21,7 @@ jobs:
     permissions:
       contents: read
     steps:
-      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+      - uses: actions/checkout@v7
         with:
           persist-credentials: false
       - name: actionlint
diff --git a/pyproject.toml b/pyproject.toml
index e91c2f0..e71d6c5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,11 +1,11 @@
 [project]
 name = "release-workflows"
-version = "1.1.0"
+version = "1.1"
 requires-python = ">=3.11"
 dependencies = ["packaging", "pyyaml"]
 
 [dependency-groups]
-dev = ["pytest==9.0.3", "ruff==0.15.10", "actionlint-py==1.7.12.24"]
+dev = ["pytest==9.0.2", "ruff==0.15.10", "actionlint-py==1.7.12.24"]
 
 [tool.ruff]
 line-length = 160

Comment thread README.md
Comment on lines +33 to +46
Call `dev-security.yml` from a workflow that runs on `pull_request`, with a job named `dev-security`, so the
checks a `dev` ruleset requires are `dev-security / dependency-review` and `dev-security / workflow-lint`:

```yaml
on:
pull_request:

permissions:
contents: read

jobs:
dev-security:
uses: blacklanternsecurity/release-workflows/.github/workflows/dev-security.yml@<full sha of the tag> # v1.1.0
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 This block is what every repo's dev ruleset will copy, but no run shows dependency-review working from a caller. Please link one caller run from a fresh public repo pinned to this head SHA (see the review body).

#11 inserted the dev-security caller between the secrets example and "map it by name as above", so that sentence pointed at a block with no secrets (review note V12 on #12). Text unchanged; only its position moves.
…apping-order

README: put the secret-mapping note back under its example
@pjhartlieb

Copy link
Copy Markdown
Contributor Author

Caller run from a fresh public repository, dev-security-caller-check, holding only the README caller workflow pinned to c3bc4cb3c30af3b71f7cebf02225cb0db810a564:

  • #1 adds packaging==25.0. dev-security / dependency-review passed, and its log shows the comparison (+ packaging@25.0, no vulnerable packages at moderate or higher). dev-security / workflow-lint passed.
  • #2 is the negative control: it adds requests==2.19.1. dev-security / dependency-review failed on its five advisories, one high, so the check does compare the pull request's base and head.

The first attempt of both runs failed with "Dependency review is not supported on this repository" until the repository's dependency graph was turned on. The dependency graph is also off on radixtarget, blastdns and blasthttp, among others, so turning it on is now a step before a repository requires these checks.

V12: #13 moves the secret-mapping paragraph back under the example it refers to. dev-security.yml is unchanged.

@pjhartlieb
pjhartlieb requested a review from en0f November 9, 2026 15:28
@en0f
en0f merged commit 1213919 into stable Nov 9, 2026
25 checks passed
@pjhartlieb pjhartlieb mentioned this pull request Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants