Repository navigation
feat(governance): complete #1234 rulings (D304, D332) and execution queue (A1–A7) #1324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -69,6 +69,12 @@ on: | |
| required: false | ||
| RADICLE_KEY: | ||
| required: false | ||
| R2_ACCESS_KEY_ID: | ||
| required: false | ||
| R2_SECRET_ACCESS_KEY: | ||
| required: false | ||
| R2_AGE_RECIPIENT: | ||
| required: false | ||
|
|
||
| permissions: | ||
| actions: read | ||
|
|
@@ -443,3 +449,62 @@ jobs: | |
| if: ${{ env.RADICLE_KEY == '' }} | ||
| run: | | ||
| echo "::notice::RADICLE_MIRROR_ENABLED=true but secrets.RADICLE_KEY is empty. Skipping Radicle mirror. Configure the RADICLE_KEY org/repo secret to enable." | ||
|
|
||
| mirror-r2-bundle: | ||
| # D303/D304: Cloudflare R2 git-bundle backup with mandatory client-side | ||
| # age encryption before any bytes leave the runner. | ||
| timeout-minutes: 20 | ||
| runs-on: ${{ inputs.runs-on }} | ||
| if: vars.R2_BUNDLE_MIRROR_ENABLED == 'true' | ||
|
Comment on lines
+453
to
+458
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The repository's actual entry point, Useful? React with 👍 / 👎. |
||
| env: | ||
| R2_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | ||
| R2_SECRET_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | ||
| R2_RECIPIENT: ${{ secrets.R2_AGE_RECIPIENT || vars.R2_AGE_RECIPIENT }} | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
| - name: Verify R2 endpoint and age recipient | ||
| if: ${{ env.R2_KEY_ID != '' && env.R2_SECRET_KEY != '' && env.R2_RECIPIENT != '' }} | ||
| env: | ||
| R2_ENDPOINT: ${{ vars.R2_ENDPOINT_URL }} | ||
| R2_BUCKET: ${{ vars.R2_BUCKET }} | ||
| AGE_RECIPIENT: ${{ env.R2_RECIPIENT }} | ||
| run: | | ||
| set -euo pipefail | ||
| if [[ ! "$R2_ENDPOINT" =~ ^https://[a-zA-Z0-9-]+(\.eu|\.fedramp|\.us)?\.r2\.cloudflarestorage\.com$ ]] || | ||
| [[ ! "$R2_BUCKET" =~ ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$ ]] || | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject dots in R2 bucket names. This expression accepts a value such as 🤖 Prompt for AI Agents |
||
| [[ ! "$AGE_RECIPIENT" =~ ^(age1[a-z0-9]{58}|ssh-ed25519[[:space:]]+AAAA[A-Za-z0-9+/=]+)$ ]]; then | ||
| echo "::error::Configure valid R2_ENDPOINT_URL (*.r2.cloudflarestorage.com), R2_BUCKET, and R2_AGE_RECIPIENT (age1... or ssh-ed25519)" | ||
| exit 1 | ||
| fi | ||
| - name: Create, age-encrypt, and upload git bundle to Cloudflare R2 | ||
| if: ${{ env.R2_KEY_ID != '' && env.R2_SECRET_KEY != '' && env.R2_RECIPIENT != '' }} | ||
| env: | ||
| AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | ||
| AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | ||
| AWS_DEFAULT_REGION: auto | ||
| R2_ENDPOINT: ${{ vars.R2_ENDPOINT_URL }} | ||
| R2_BUCKET: ${{ vars.R2_BUCKET }} | ||
| AGE_RECIPIENT: ${{ env.R2_RECIPIENT }} | ||
| MIRROR_ORG: ${{ vars.R2_ORG || vars.MIRROR_ORG || github.repository_owner }} | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| run: | | ||
| set -euo pipefail | ||
| if ! command -v age >/dev/null 2>&1; then | ||
| sudo apt-get update -qq && sudo apt-get install -y -qq age | ||
| fi | ||
| RAW_BUNDLE="$RUNNER_TEMP/${REPO_NAME}.bundle" | ||
| ENC_BUNDLE="$RUNNER_TEMP/${REPO_NAME}.bundle.age" | ||
| trap 'rm -f "$RAW_BUNDLE" "$ENC_BUNDLE"' EXIT | ||
| git bundle create "$RAW_BUNDLE" --all | ||
| git bundle verify "$RAW_BUNDLE" | ||
| age -r "$AGE_RECIPIENT" -o "$ENC_BUNDLE" "$RAW_BUNDLE" | ||
| rm -f "$RAW_BUNDLE" | ||
| test -s "$ENC_BUNDLE" | ||
| aws s3 cp "$ENC_BUNDLE" "s3://${R2_BUCKET}/${MIRROR_ORG}/${REPO_NAME}/latest.bundle.age" --endpoint-url "$R2_ENDPOINT" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For an R2 bucket without separately configured object versioning, every run overwrites the same Useful? React with 👍 / 👎. |
||
| - name: Skipped (R2 credentials or age recipient not configured) | ||
| if: ${{ env.R2_KEY_ID == '' || env.R2_SECRET_KEY == '' || env.R2_RECIPIENT == '' }} | ||
| run: | | ||
| echo "::notice::R2_BUNDLE_MIRROR_ENABLED=true but R2 credentials or R2_AGE_RECIPIENT are empty. Skipping R2 bundle upload." | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,9 +36,20 @@ Mirror organisation selection is `<FORGE>_ORG`, then `MIRROR_ORG`, then the | |
| GitHub repository owner. This preserves per-forge destinations while allowing | ||
| a shared destination for transferred repositories. | ||
|
|
||
| Per owner rulings D303 and D304 (`standards#1234`), `mirror-reusable.yml` also | ||
| provides an optional `mirror-r2-bundle` job gated on | ||
| `vars.R2_BUNDLE_MIRROR_ENABLED == 'true'`. When enabled and provisioned with | ||
| `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_ENDPOINT_URL` | ||
| (`https://<account>.r2.cloudflarestorage.com`), `R2_BUCKET`, and | ||
| `R2_AGE_RECIPIENT` (`age1...` or `ssh-ed25519`), the runner creates a full | ||
|
Comment on lines
+41
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,80p' docs/MIRROR-HOST-VERIFICATION.adoc
rg -n 'secrets|inherit' docs/MIRROR-HOST-VERIFICATION.adocRepository: hyperpolymath/standards Length of output: 3320 🏁 Script executed: # Locate R2 job, secret references, and the reusable-workflow declarations.
rg -n -F -e 'mirror-r2-bundle' -e 'R2_' -e 'secrets:' -e 'workflow_call' -e 'inherit' -- .github/workflows/mirror-reusable.yml || test "$?" -eq 1
# Find caller workflows that reference the reusable mirror workflow or R2 secrets.
rg -n -F -e 'mirror-reusable.yml' -e 'R2_ACCESS_KEY_ID' -e 'R2_AGE_RECIPIENT' -e 'R2_BUNDLE_MIRROR_ENABLED' -- .github docs scripts || test "$?" -eq 1Repository: hyperpolymath/standards Length of output: 12935 Document the R2 secret mappings for callers. The R2 paragraph does not tell callers to pass R2 secrets through
📝 Suggested documentation addition `plaintext bundle`, and uploads only the `.bundle.age` ciphertext to Cloudflare
R2.
+
+Callers must pass the R2 secrets explicitly in the `secrets:` block. GitHub does
+not pass caller secrets to a reusable workflow automatically. Without these
+mappings, the `mirror-r2-bundle` job skips the upload and emits only a notice.
+
+[source,yaml]
+----
+jobs:
+ mirror:
+ uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@<sha>
+ secrets:
+ R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
+ R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
+ R2_AGE_RECIPIENT: ${{ secrets.R2_AGE_RECIPIENT }}
+----🤖 Prompt for AI Agents |
||
| `git bundle`, verifies it, encrypts it client-side with `age -r`, removes the | ||
| plaintext bundle, and uploads only the `.bundle.age` ciphertext to Cloudflare | ||
| R2. | ||
|
|
||
| `scripts/tests/science-ci-security-test.sh` launches the assertions in | ||
| `scripts/tests/science-ci-security-test.rb`, which exercise every actual SSH | ||
| verification step with the real `ssh-keygen` fingerprint implementation. It | ||
| tests approved, tampered, absent, malformed, and unapproved keys, including | ||
| that failure cannot export a trusted SSH configuration. The same suite checks | ||
| that failure cannot export a trusted SSH configuration, plus the R2 endpoint | ||
| and `age` recipient verification gate. The same suite checks | ||
| that the Hypatia source checkout matches its resolved cache identity. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: hyperpolymath/standards
Length of output: 5134
🏁 Script executed:
Repository: hyperpolymath/standards
Length of output: 11857
🏁 Script executed:
Repository: hyperpolymath/standards
Length of output: 25859
Wire the direct caller in a post-merge follow-up.
.github/workflows/mirror.ymlis the only caller. It pinsmirror-reusable.ymltod55304a01b5260b0511aae0d14ed8b8770b00293and maps only the seven existing mirror secrets.After the caller points to the landed reusable-workflow revision, add the two R2 credential mappings. Without them, the R2 job skips the upload when enabled.
R2_AGE_RECIPIENTcan come from eithersecrets.R2_AGE_RECIPIENTorvars.R2_AGE_RECIPIENT, so only the two access credentials are unconditionally missing from this caller.Do not pin the caller to an unstable PR-only revision. Repin it after the reusable workflow lands on a stable revision.
Suggested secret mappings
RADICLE_KEY: ${{ secrets.RADICLE_KEY }} + R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + R2_AGE_RECIPIENT: ${{ secrets.R2_AGE_RECIPIENT }}🤖 Prompt for AI Agents