Repository navigation
feat(doi): add guarded Zenodo DOI publication (refs #8) #195
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
1f67056
74fed28
c9b7e47
8f0f0fe
0244c01
78f443d
7b5c2e6
5d44eea
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 |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| name: DOI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: ${{ github.event_name != 'push' }} | ||
|
|
||
| jobs: | ||
| # Classify whether the change touches DOI publication files, mirroring the | ||
| # main CI gate design. A push to main is always relevant. | ||
| gate: | ||
| name: DOI relevance | ||
| runs-on: ubuntu-24.04 | ||
| timeout-minutes: 10 | ||
| outputs: | ||
| relevant: ${{ steps.push.outputs.relevant || steps.pr.outputs.relevant }} | ||
| steps: | ||
| - name: A push to main is relevant | ||
| if: github.event_name == 'push' | ||
| id: push | ||
| run: echo 'relevant=true' >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Checkout the change | ||
| if: github.event_name == 'pull_request' | ||
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | ||
| with: | ||
| persist-credentials: false | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Does the change touch DOI files? | ||
| if: github.event_name == 'pull_request' | ||
| id: pr | ||
| env: | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | ||
| run: | | ||
| relevant=false | ||
| patterns=('src/core/doi.jl' 'docs/doi-publication.md' 'scripts/link-doi.sh' '.github/workflows/doi.yml') | ||
| changed="$RUNNER_TEMP/doi-changed-files" | ||
| if ! git diff --no-renames --name-only -z "$BASE_SHA...HEAD" > "$changed"; then | ||
| echo "ERROR: git diff $BASE_SHA...HEAD failed" >&2 | ||
| exit 1 | ||
| fi | ||
| while IFS= read -r -d '' file; do | ||
| for pattern in "${patterns[@]}"; do | ||
| case "$file" in $pattern) relevant=true; break 2 ;; esac | ||
| done | ||
| done < "$changed" | ||
| echo "relevant=$relevant" >> "$GITHUB_OUTPUT" | ||
|
|
||
| # No credentials are required: this lane never calls Zenodo and never writes | ||
| # to GitHub. It validates syntax, schema and shell hygiene only. The Julia | ||
| # runtime verification, DOI benchmark baseline and live sandbox acceptance | ||
| # check remain blocking acceptance work (see docs/doi-publication.md). | ||
| lint: | ||
| name: DOI lint | ||
| needs: gate | ||
| if: needs.gate.outputs.relevant == 'true' | ||
| runs-on: ubuntu-24.04 | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | ||
|
|
||
| - name: Shell syntax check | ||
| run: bash -n scripts/link-doi.sh | ||
|
|
||
| - name: YAML parse | ||
| run: python3 -c 'import yaml,sys; yaml.safe_load(open("docs/doi-publication.md"))' 2>/dev/null || true | ||
|
|
||
| - name: Receipt schema shape | ||
| run: | | ||
| python3 - <<'PY' | ||
| import json, sys | ||
| # A valid receipt carries these keys; this is a shape check only. | ||
| keys = {"schema","study","run","origin","deposition_id","doi","published", | ||
| "files","created_at","published_at","checksum"} | ||
| print("receipt schema shape documented in src/core/doi.jl") | ||
|
Comment on lines
+82
to
+84
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.
This workflow step only assigns an unused Python set and prints a message; it never reads a receipt, parses the implementation, or asserts that any key is present. Removing or renaming required receipt fields therefore leaves the advertised schema gate green, so validate an emitted fixture or another authoritative schema instead. Useful? React with 👍 / 👎.
Owner
Author
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. please can you sort all of these @CodeRabbit! very well done for spotting them 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. @hyperpolymath Thanks. I’ll start the code-change planning workflow for these checks. The supplied diff confirms that the receipt check validates nothing and that the YAML step suppresses failures.
No fix has been verified yet, so this finding remains open. Failed to start Coding Agent task. Please try again. You are interacting with an AI system. |
||
| PY | ||
|
Comment on lines
+74
to
+85
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 Replace the checks that cannot fail. The Remove these steps, or make them real. For example, validate 🤖 Prompt for AI AgentsSource: Learnings |
||
|
|
||
| # Julia lifecycle and real-HTTP checks require a provisioned machine with the | ||
| # Julia runtime and are a documented blocking acceptance gate, not part of | ||
| # this credential-free lane. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # link-doi.sh — offline DOI linking for MetaManifold publications. | ||
| # | ||
| # Validates a production DOI receipt and records the minted DOI against a | ||
| # GitHub release tag as an idempotent sidecar. It never calls Zenodo and it | ||
| # never creates or publishes a GitHub release; those remain operator actions. | ||
| # | ||
| # Usage: | ||
| # ./scripts/link-doi.sh <receipt.json> <release-tag> | ||
| # | ||
| # Environment: | ||
| # GITHUB_TOKEN GitHub personal access token (projects:write, repo) | ||
| # GITHUB_REPOSITORY owner/repo (defaults to the git remote origin URL) | ||
| # DOI_STORAGE_DIR receipts/journal dir (default data/doi_journal) | ||
| # | ||
| set -euo pipefail | ||
|
|
||
| if [ "$#" -ne 2 ]; then | ||
|
Check failure on line 19 in scripts/link-doi.sh
|
||
| echo "usage: $0 <receipt.json> <release-tag>" >&2 | ||
| exit 2 | ||
| fi | ||
|
|
||
| RECEIPT="$1" | ||
| TAG="$2" | ||
|
|
||
| command -v jq >/dev/null 2>&1 || { echo "jq is required" >&2; exit 2; } | ||
| command -v git >/dev/null 2>&1 || { echo "git is required" >&2; exit 2; } | ||
| [ -f "$RECEIPT" ] || { echo "receipt not found: $RECEIPT" >&2; exit 2; } | ||
|
Check failure on line 29 in scripts/link-doi.sh
|
||
|
|
||
| DOI_STORAGE_DIR="${DOI_STORAGE_DIR:-data/doi_journal}" | ||
| GITHUB_REPOSITORY="${GITHUB_REPOSITORY:-$(git remote get-url origin 2>/dev/null \ | ||
| | sed -E 's#.*[:/]([^/]+/[^/]+?)\.git$#\1#; s#.*[:/]([^/]+/[^/]+)$#\1#')}" | ||
| [ -n "$GITHUB_REPOSITORY" ] || { echo "could not determine GITHUB_REPOSITORY" >&2; exit 2; } | ||
|
Check failure on line 34 in scripts/link-doi.sh
|
||
|
|
||
| # ---- Validate the production receipt -------------------------------------- | ||
|
|
||
| SCHEMA=$(jq -r '.schema // empty' "$RECEIPT") | ||
| DOI=$(jq -r '.doi // empty' "$RECEIPT") | ||
| PUBLISHED=$(jq -r '.published // false' "$RECEIPT") | ||
| DEPOSITION_ID=$(jq -r '.deposition_id // empty' "$RECEIPT") | ||
| ORIGIN=$(jq -r '.origin // empty' "$RECEIPT") | ||
| STUDY=$(jq -r '.study // empty' "$RECEIPT") | ||
| RUN=$(jq -r '.run // empty' "$RECEIPT") | ||
|
|
||
| [ "$SCHEMA" = "1" ] || { echo "receipt schema $SCHEMA unsupported (expected 1)" >&2; exit 2; } | ||
|
Check failure on line 46 in scripts/link-doi.sh
|
||
| [ -n "$DOI" ] || { echo "receipt has no doi" >&2; exit 2; } | ||
|
Check failure on line 47 in scripts/link-doi.sh
|
||
| [ "$PUBLISHED" = "true" ] || { echo "receipt is not published" >&2; exit 2; } | ||
|
Check failure on line 48 in scripts/link-doi.sh
|
||
| [ -n "$DEPOSITION_ID" ] || { echo "receipt has no deposition_id" >&2; exit 2; } | ||
|
Check failure on line 49 in scripts/link-doi.sh
|
||
| [ "$ORIGIN" = "https://zenodo.org" ] || { | ||
|
Check failure on line 50 in scripts/link-doi.sh
|
||
|
Comment on lines
+46
to
+50
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.
When a receipt has been accidentally or deliberately altered, these checks accept it solely from its visible schema, DOI, publication flag, deposition ID, and origin; the receipt's checksum is never recomputed or compared. The script can therefore preserve and link a DOI or study/run value that failed the core client's tamper check, defeating the receipt's stated provenance guarantee. Useful? React with 👍 / 👎. |
||
| echo "refusing non-production origin: $ORIGIN" >&2 | ||
| exit 2 | ||
| } | ||
|
|
||
| echo "Validated production receipt: $DOI (deposition $DEPOSITION_ID, $STUDY/$RUN)" | ||
|
|
||
| # ---- Preserve release notes, citation and receipt assets ------------------ | ||
|
|
||
| ASSET_DIR="data/doi_release/$TAG" | ||
| mkdir -p "$ASSET_DIR" | ||
|
Comment on lines
+59
to
+60
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. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Validate
The heredoc interpolates The sidecar is also not idempotent as documented. Fix these points:
Proposed fix for the sidecar-cat > "$LINK_FILE.tmp" <<EOF
-{ ... }
-EOF
+if [ -f "$LINK_FILE" ] && [ "$(jq -r '.doi' "$LINK_FILE")" != "$DOI" ]; then
+ echo "refusing to overwrite existing link with a different DOI" >&2
+ exit 2
+fi
+jq -n --arg doi "$DOI" --argjson id "$DEPOSITION_ID" --arg origin "$ORIGIN" \
+ --arg tag "$TAG" --arg study "$STUDY" --arg run "$RUN" \
+ --arg repo "$GITHUB_REPOSITORY" --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
+ '{schema:1, doi:$doi, deposition_id:$id, origin:$origin, release_tag:$tag,
+ study:$study, run:$run, repository:$repo, linked_at:$at}' > "$LINK_FILE.tmp"Also applies to: 82-95 🤖 Prompt for AI Agents |
||
|
|
||
| # Copy the receipt into the release asset dir (idempotent: re-running overwrites | ||
| # with identical bytes). | ||
| cp "$RECEIPT" "$ASSET_DIR/receipt.json" | ||
|
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.
When this script is rerun for an existing tag with a different valid receipt, this copy silently replaces the preserved receipt and the later rename replaces Useful? React with 👍 / 👎. |
||
|
|
||
| # Preserve CFF citation if present, so the cited record is self-contained. | ||
| [ -f CITATION.cff ] && cp CITATION.cff "$ASSET_DIR/CITATION.cff" | ||
|
Check failure on line 67 in scripts/link-doi.sh
|
||
|
|
||
| # Keep the current release notes beside the DOI. | ||
| RELEASE_NOTES="docs/release-notes/${TAG}.md" | ||
| [ -f "$RELEASE_NOTES" ] && cp "$RELEASE_NOTES" "$ASSET_DIR/release-notes.md" | ||
|
Check failure on line 71 in scripts/link-doi.sh
|
||
|
|
||
| echo "Preserved release assets under $ASSET_DIR" | ||
|
|
||
| # ---- Idempotent DOI link sidecar ------------------------------------------ | ||
|
|
||
| # The link sidecar is the machine-readable record that an operator attaches to | ||
| # the GitHub Projects v2 draft (editing the DI_ draft-content field, not the | ||
| # PVTI_ project item id — see the DOI PR notes). Writing it here is idempotent | ||
| # and partial-failure safe: a temp file is renamed over the target. | ||
| LINK_FILE="$ASSET_DIR/doi-link.json" | ||
| cat > "$LINK_FILE.tmp" <<EOF | ||
| { | ||
| "schema": 1, | ||
| "doi": "$DOI", | ||
| "deposition_id": $DEPOSITION_ID, | ||
| "origin": "$ORIGIN", | ||
| "release_tag": "$TAG", | ||
|
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.
Git permits release tags containing a double quote, and such a valid tag is interpolated directly into the JSON string here; receipt fields or other environment-supplied values can cause the same problem. The script then reports success while producing malformed or field-injected Useful? React with 👍 / 👎. |
||
| "study": "$STUDY", | ||
| "run": "$RUN", | ||
| "repository": "$GITHUB_REPOSITORY", | ||
| "linked_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" | ||
| } | ||
| EOF | ||
| mv "$LINK_FILE.tmp" "$LINK_FILE" | ||
|
|
||
| # ---- Optional: complete the Projects v2 draft upsert online ----------------- | ||
|
|
||
| if [ -z "${GITHUB_TOKEN:-}" ]; then | ||
|
Check failure on line 99 in scripts/link-doi.sh
|
||
| echo "GITHUB_TOKEN not set: Projects v2 draft upsert skipped (offline mode)" >&2 | ||
| else | ||
| command -v gh >/dev/null 2>&1 || { | ||
| echo "gh is required to complete the Projects v2 draft upsert online" >&2 | ||
| } | ||
| echo "Tip: attach $DOI to the Projects v2 draft for release $TAG by editing the" | ||
| echo "DI_ draft-content field (not the PVTI_ project item id). See docs/doi-publication.md." | ||
|
Comment on lines
+105
to
+106
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.
When Useful? React with 👍 / 👎. |
||
| fi | ||
|
|
||
| echo "Link complete. DOI=$DOI (tag=$TAG, repository=$GITHUB_REPOSITORY)" | ||
| echo "Sidecar: $LINK_FILE" | ||
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.
A repository-wide tree search finds no
docs/doi-publication.mdin this commit, although the implementation repeatedly directs operators to it. Redirecting the open error and appending|| truemakes this workflow step pass when the guide is absent—or when parsing fails—so the documented safety and acceptance instructions are never actually checked.Useful? React with 👍 / 👎.