From 832f7e044979ea3b666c59437924f8b25b8c4102 Mon Sep 17 00:00:00 2001 From: Michael van Tellingen Date: Sun, 25 Oct 2026 10:14:02 +0100 Subject: [PATCH 1/2] Exempt labd/* actions from pinact min-age Pass a pinact config (written to RUNNER_TEMP) instead of the min_age input. The config keeps a 7 day minimum release age for all actions but sets it to 0 for actions owned by labd, so our own fresh releases are not rejected org-wide for a week. --- .github/workflows/security.yaml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index abe9e47..b5ac307 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -23,13 +23,32 @@ jobs: with: persist-credentials: false + # Required workflows run in the context of the target repository, so a + # config file committed here is not checked out. Write it to RUNNER_TEMP. + # Third-party actions must be at least 7 days old; first-party labd/* + # actions are exempt so our own fresh releases can be adopted right away. + # Note: do not set the `min_age` input, the --min-age flag overrides rules. + - name: Write pinact config + if: github.event.pull_request.head.repo.full_name == github.repository + run: | + cat > "$RUNNER_TEMP/pinact.yaml" <<'EOF' + version: 3 + min_age: + value: 7 + always: true + rules: + - min_age: 0 + conditions: + - expr: ActionRepoOwner == "labd" + EOF + - name: Pin actions if: github.event.pull_request.head.repo.full_name == github.repository uses: suzuki-shunsuke/pinact-action@896d595f299e71d65b9d28349d6956abe144390a # v3.0.0 with: skip_push: true verify: true - min_age: 7 + config: ${{ runner.temp }}/pinact.yaml - name: Run zizmor uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 From 4bef21882003207b954a4c7e037861b6aca7ffb2 Mon Sep 17 00:00:00 2001 From: Michael van Tellingen Date: Sun, 25 Oct 2026 10:22:40 +0100 Subject: [PATCH 2/2] Merge the repository's own pinact config into the org check Instead of replacing a repository's pinact config, detect it using pinact's discovery order and merge it with the org policy: repo settings (files, ignore_actions, separator, ghes, rules) are kept, min_age is raised to at least 7 days with always: true, repo rules that lower min_age below that are dropped, and the labd/* exemption is appended. Configs with a schema version other than 3 are used as-is with PINACT_MIN_AGE enforcing the org minimum. --- .github/workflows/security.yaml | 80 ++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 16 deletions(-) diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index b5ac307..98d4ecb 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -23,24 +23,71 @@ jobs: with: persist-credentials: false - # Required workflows run in the context of the target repository, so a - # config file committed here is not checked out. Write it to RUNNER_TEMP. - # Third-party actions must be at least 7 days old; first-party labd/* - # actions are exempt so our own fresh releases can be adopted right away. - # Note: do not set the `min_age` input, the --min-age flag overrides rules. + # Build the pinact config for the org-wide check. Required workflows run + # in the context of the target repository, so it is written to + # RUNNER_TEMP rather than committed here. + # + # Policy: every action must be at least ORG_MIN_AGE days old, except + # first-party labd/* actions. A repository's own pinact config is merged + # in (files, ignore_actions, separator, ghes, rules are kept), but it can + # only raise the minimum age, never lower it: repo rules that lower + # min_age below the org minimum have that override removed. + # + # Do not set the action's `min_age` input: --min-age overrides all rules. - name: Write pinact config + id: pinact-config if: github.event.pull_request.head.repo.full_name == github.repository + env: + ORG_MIN_AGE: "7" run: | - cat > "$RUNNER_TEMP/pinact.yaml" <<'EOF' - version: 3 - min_age: - value: 7 - always: true - rules: - - min_age: 0 - conditions: - - expr: ActionRepoOwner == "labd" - EOF + set -euo pipefail + out="$RUNNER_TEMP/pinact.yaml" + + # Same discovery order as pinact (pkg/config findProjectConfigPath). + repo_cfg="" + for p in .pinact.yaml .github/pinact.yaml .pinact.yml .github/pinact.yml; do + if [ -f "$p" ]; then + repo_cfg="$p" + break + fi + done + + if [ -z "$repo_cfg" ]; then + echo "No repository pinact config found, using the org default" + printf '%s\n' 'version: 3' > "$out" + elif [ "$(yq '.version // 0' "$repo_cfg")" != "3" ]; then + # Unknown schema version: we can't merge safely. Use the repo + # config as-is and enforce the org minimum for every action + # (including labd/*) via PINACT_MIN_AGE. + echo "::warning file=$repo_cfg::pinact config schema version is not 3; org min-age policy is applied to all actions without the labd exemption" + echo "PINACT_MIN_AGE=$ORG_MIN_AGE" >> "$GITHUB_ENV" + echo "path=$repo_cfg" >> "$GITHUB_OUTPUT" + exit 0 + else + echo "Merging repository pinact config $repo_cfg with the org policy" + # `files` patterns are relative to the config file's directory. + CFG_DIR=$(python3 -c 'import os, sys; print(os.path.relpath(os.path.dirname(os.path.abspath(sys.argv[1])), sys.argv[2]))' "$repo_cfg" "$RUNNER_TEMP") + export CFG_DIR + lowered=$(yq '[.rules[]? | select(has("min_age") and .min_age < env(ORG_MIN_AGE))] | length' "$repo_cfg") + if [ "$lowered" != "0" ]; then + echo "::warning file=$repo_cfg::$lowered rule(s) lower min_age below the org minimum of $ORG_MIN_AGE days; these overrides are ignored by the org check (labd/* actions are always exempt)" + fi + yq ' + (select(has("files")) | .files[].pattern) |= strenv(CFG_DIR) + "/" + . | + (.rules[]? | select(has("min_age") and .min_age < env(ORG_MIN_AGE))) |= del(.min_age) | + .rules = [(.rules // [])[] | select(has("min_age") or has("ignore"))] + ' "$repo_cfg" > "$out" + fi + + yq -i ' + .min_age.value = ([(.min_age.value // 0), env(ORG_MIN_AGE)] | max) | + .min_age.always = true | + .rules = (.rules // []) + [{"min_age": 0, "conditions": [{"expr": "ActionRepoOwner == \"labd\""}]}] + ' "$out" + + echo "Effective pinact config:" + cat "$out" + echo "path=$out" >> "$GITHUB_OUTPUT" - name: Pin actions if: github.event.pull_request.head.repo.full_name == github.repository @@ -48,7 +95,8 @@ jobs: with: skip_push: true verify: true - config: ${{ runner.temp }}/pinact.yaml + verify_min_age: true + config: ${{ steps.pinact-config.outputs.path }} - name: Run zizmor uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2