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
120 changes: 120 additions & 0 deletions .github/workflows/artifact-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Artifact cleanup

# Deletes GitHub Actions artifacts older than a threshold so storage doesn't
# accumulate and trip the account-wide quota.
#
# Three triggers:
# - schedule: Monday 06:00 UTC, same slot as branch-protection-sync,
# security, and dependabot for weekly-governance clustering.
# - workflow_dispatch: on-demand cleanup with inputs for threshold +
# dry-run safety.
# - push: not wired — cleanup doesn't need to fire on code changes.
#
# Uses the default GITHUB_TOKEN (built-in `actions: write` scope is
# sufficient for artifact deletion — no PAT needed). The scheduled run
# uses the hard-coded default (7 days, live delete); manual runs default
# to dry-run for safety.

on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
inputs:
days_to_keep:
description: "Delete artifacts older than this many days"
required: true
default: "7"
dry_run:
description: "List only, no deletion (safe to flip to false when ready)"
type: boolean
default: true

permissions:
actions: write # for artifact deletion
contents: read

concurrency:
group: artifact-cleanup
cancel-in-progress: false

jobs:
cleanup:
name: Prune old artifacts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Compute threshold
id: threshold
env:
DAYS: ${{ inputs.days_to_keep || '7' }}
run: |
days="${DAYS}"
threshold_secs=$(( days * 86400 ))
echo "days=${days}" >> "$GITHUB_OUTPUT"
echo "secs=${threshold_secs}" >> "$GITHUB_OUTPUT"
echo "::notice::Pruning artifacts older than ${days} day(s)."

- name: List candidates
id: list
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
THRESHOLD_SECS: ${{ steps.threshold.outputs.secs }}
run: |
gh api --paginate \
"/repos/${GITHUB_REPOSITORY}/actions/artifacts?per_page=100" \
--jq ".artifacts[]
| select((now - (.created_at | fromdateiso8601)) > ${THRESHOLD_SECS})
| [.id, .created_at,
((.size_in_bytes/1024/1024*100|floor)/100|tostring),
(.workflow_run.name // \"<unknown>\"),
.name]
| @tsv" \
> candidates.tsv
count=$(wc -l < candidates.tsv | tr -d ' ')
echo "count=${count}" >> "$GITHUB_OUTPUT"
echo "::notice::Found ${count} artifact(s) older than ${{ steps.threshold.outputs.days }} day(s)."

- name: Summary (candidates)
if: always()
run: |
{
echo "### Artifact cleanup — dry_run=${{ inputs.dry_run || 'false (scheduled)' }}"
echo ""
echo "**Threshold:** older than ${{ steps.threshold.outputs.days }} day(s)"
echo "**Matched:** ${{ steps.list.outputs.count }} artifact(s)"
echo ""
echo "| id | created_at | size (MB) | workflow | name |"
echo "|---|---|---|---|---|"
awk -F'\t' '{ printf "| %s | %s | %s | %s | %s |\n", $1, $2, $3, $4, $5 }' \
candidates.tsv || true
} >> "$GITHUB_STEP_SUMMARY"

- name: Delete (skipped when dry_run=true)
if: ${{ inputs.dry_run == false || inputs.dry_run == 'false' || github.event_name == 'schedule' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Individual 404s are expected — an artifact can expire between the
# list call and the delete call. A mixed outcome still passes. Only
# the every-attempt-failed signal escalates (token lost
# `actions: write`, API rate-limit, network flake).
deleted=0
failed=0
while IFS=$'\t' read -r id _rest; do
if [ -n "${id}" ]; then
if gh api -X DELETE "/repos/${GITHUB_REPOSITORY}/actions/artifacts/${id}" > /dev/null 2>&1; then
deleted=$((deleted + 1))
else
echo "::warning::failed to delete artifact ${id}"
failed=$((failed + 1))
fi
fi
done < candidates.tsv
echo "::notice::Deleted ${deleted} artifact(s); ${failed} failure(s)."
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Deleted:** ${deleted} artifact(s); ${failed} failure(s)." >> "$GITHUB_STEP_SUMMARY"
if [ "${failed}" -gt 0 ] && [ "${deleted}" -eq 0 ]; then
echo "::error::All ${failed} delete attempts failed. Check token scope (actions: write) or API rate limits."
exit 1
fi
95 changes: 95 additions & 0 deletions .github/workflows/branch-protection.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Branch protection sync

# Applies the declarative branch-protection configuration in
# .github/branch-protection/*.json to main and develop. See that directory's
# README for rationale and the trigger matrix.

on:
schedule:
# Weekly drift re-assertion, Monday 06:00 UTC. Same slot as the security
# workflow and Dependabot for governance-activity clustering.
- cron: "0 6 * * 1"
workflow_dispatch:
push:
branches: [main]
paths:
- .github/branch-protection/**
- .github/workflows/branch-protection.yml

permissions:
contents: read

# Serialise concurrent applies. A scheduled run, a manual dispatch, and a
# push landing around the same time could otherwise race on the same PUT.
# cancel-in-progress: false — never interrupt a protection apply mid-flight.
concurrency:
group: branch-protection-sync
cancel-in-progress: false

jobs:
apply:
name: Apply ${{ matrix.branch }} protection
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
branch: [main, develop]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Verify token is configured
env:
GH_TOKEN: ${{ secrets.BRANCH_PROTECTION_TOKEN }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::error::BRANCH_PROTECTION_TOKEN secret is not set."
echo "::error::See docs/DEVELOPMENT.md#branch-protection-sync-setup."
exit 1
fi

- name: Snapshot current protection (before)
env:
GH_TOKEN: ${{ secrets.BRANCH_PROTECTION_TOKEN }}
run: |
# May 404 if protection doesn't exist yet (first run). That's fine.
gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${{ github.repository }}/branches/${{ matrix.branch }}/protection" \
> before.json 2>/dev/null || echo '{"note": "no protection set before run"}' > before.json

- name: Apply ${{ matrix.branch }} protection
env:
GH_TOKEN: ${{ secrets.BRANCH_PROTECTION_TOKEN }}
run: |
gh api \
--method PUT \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/${{ github.repository }}/branches/${{ matrix.branch }}/protection" \
--input ".github/branch-protection/${{ matrix.branch }}.json"

- name: Snapshot current protection (after)
env:
GH_TOKEN: ${{ secrets.BRANCH_PROTECTION_TOKEN }}
run: |
gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${{ github.repository }}/branches/${{ matrix.branch }}/protection" \
> after.json

- name: Summary (with before → after diff)
if: always()
run: |
{
echo "### Branch protection: ${{ matrix.branch }}"
echo ""
echo "**Config source:** \`.github/branch-protection/${{ matrix.branch }}.json\`"
echo ""
echo "**Before → after diff** (empty block = no drift was present):"
echo ""
echo '```diff'
diff -u before.json after.json || true
echo '```'
echo ""
echo "Review current state at https://github.com/${{ github.repository }}/settings/branches"
} >> "$GITHUB_STEP_SUMMARY"
55 changes: 55 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# CodeQL — placeholder (disabled).
#
# CodeQL on a private personal repo requires GitHub Advanced Security (GHAS),
# which is only available on Enterprise plans. When this repo either:
# (a) becomes public, or
# (b) gains a code-scanning subscription (GHAS or equivalent),
# re-activate by replacing the `on:` block below with:
#
# on:
# push:
# branches: [main, develop]
# pull_request:
# branches: [main, develop]
# schedule:
# - cron: "0 6 * * 1"
#
# Until then the workflow is manual-trigger-only (workflow_dispatch) so it
# does not fire on pushes/PRs and does not pollute CI with guaranteed
# failures.

name: CodeQL

on:
workflow_dispatch:

permissions:
actions: read
contents: read
security-events: write

jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- language: python
build-mode: none
- language: javascript-typescript
build-mode: none
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
Loading