From ad9e3d21e98f97aee2df325652ea21bf9487a9b4 Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 10:37:55 -0700 Subject: [PATCH 01/12] Add GitHub Actions workflow to build PDF with decktape On every push to main that touches security-in-age-of-ai/, build a PDF from the Reveal.js deck using decktape and commit it back to the same directory. PDF is also uploaded as a workflow artifact. --- .github/workflows/build-pdf.yml | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/build-pdf.yml diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml new file mode 100644 index 0000000..69b5bd8 --- /dev/null +++ b/.github/workflows/build-pdf.yml @@ -0,0 +1,59 @@ +name: Build Presentation PDFs + +on: + push: + branches: [main] + paths: + - 'security-in-age-of-ai/**' + - '.github/workflows/build-pdf.yml' + workflow_dispatch: + +# Practice what the deck preaches: deny by default, opt in per job. +permissions: {} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write # commit the generated PDF back to the branch + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: true # needed for the commit-back step + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install decktape + run: npm install -g decktape@3.12.0 + + - name: Serve repository over HTTP + # decktape's reveal.js automation needs http://, not file:// + run: npx --yes http-server . -p 8000 -s & + + - name: Wait for server + run: npx --yes wait-on http://localhost:8000 + + - name: Build security-in-age-of-ai PDF + run: | + decktape reveal \ + --size 1280x720 \ + --load-pause 500 \ + http://localhost:8000/security-in-age-of-ai/index.html \ + security-in-age-of-ai/security-in-age-of-ai.pdf + + - name: Commit PDF back to branch + # Skips the commit if there are no changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore(pdf): rebuild presentation PDFs" + file_pattern: "security-in-age-of-ai/*.pdf" + + - name: Upload PDF as workflow artifact + uses: actions/upload-artifact@v4 + with: + name: security-in-age-of-ai-pdf + path: security-in-age-of-ai/security-in-age-of-ai.pdf From 073c0fc275651854761072460728e582ae1e4929 Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 11:03:32 -0700 Subject: [PATCH 02/12] SHA-pin all actions in build-pdf workflow Pin to full 40-char commit SHAs with tag comment, matching the deck's own guidance on hijack-resistant action references. --- .github/workflows/build-pdf.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index 69b5bd8..f2aae84 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -18,12 +18,12 @@ jobs: contents: write # commit the generated PDF back to the branch steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: true # needed for the commit-back step - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '20' @@ -47,13 +47,13 @@ jobs: - name: Commit PDF back to branch # Skips the commit if there are no changes - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0 with: commit_message: "chore(pdf): rebuild presentation PDFs" file_pattern: "security-in-age-of-ai/*.pdf" - name: Upload PDF as workflow artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: security-in-age-of-ai-pdf path: security-in-age-of-ai/security-in-age-of-ai.pdf From fa80d1dc1921567838effdc087f3458292eca9cb Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 11:11:55 -0700 Subject: [PATCH 03/12] Pin decktape install to commit SHA, not version tag Install decktape from astefanutti/decktape at commit 6735cec (v3.12.0) so a retagged release on GitHub or npm can't substitute different code. --- .github/workflows/build-pdf.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index f2aae84..bbb4356 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -28,7 +28,8 @@ jobs: node-version: '20' - name: Install decktape - run: npm install -g decktape@3.12.0 + # Pin to commit SHA, not a tag - tags on npm/GitHub can be retagged + run: npm install -g github:astefanutti/decktape#6735cec96dad6b32ee43d04f858b9794081cb866 # v3.12.0 - name: Serve repository over HTTP # decktape's reveal.js automation needs http://, not file:// From 3ea7c14479c1d4a9e385efe63358c53fd58bbd40 Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 11:13:15 -0700 Subject: [PATCH 04/12] Use pnpm for global tool install and dlx invocations Enable pnpm via Corepack and switch decktape install + http-server / wait-on invocations from npm/npx to pnpm / pnpm dlx. --- .github/workflows/build-pdf.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index bbb4356..d2906d9 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -27,16 +27,22 @@ jobs: with: node-version: '20' + - name: Enable pnpm via Corepack + run: | + corepack enable + corepack prepare pnpm@9.15.0 --activate + pnpm --version + - name: Install decktape # Pin to commit SHA, not a tag - tags on npm/GitHub can be retagged - run: npm install -g github:astefanutti/decktape#6735cec96dad6b32ee43d04f858b9794081cb866 # v3.12.0 + run: pnpm add -g github:astefanutti/decktape#6735cec96dad6b32ee43d04f858b9794081cb866 # v3.12.0 - name: Serve repository over HTTP # decktape's reveal.js automation needs http://, not file:// - run: npx --yes http-server . -p 8000 -s & + run: pnpm dlx http-server . -p 8000 -s & - name: Wait for server - run: npx --yes wait-on http://localhost:8000 + run: pnpm dlx wait-on http://localhost:8000 - name: Build security-in-age-of-ai PDF run: | From ec40c23fef6e36833a6e64c37a9e6afcc1f9105c Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 11:14:58 -0700 Subject: [PATCH 05/12] Split workflow into build (PR + push) and publish (push to main only) - build: read-only permissions, runs on every PR and push to verify the deck still renders to PDF; uploads PDF as a workflow artifact. - publish: gated by 'push to main', needs: build. Downloads the verified artifact and commits it back. Never sees source or runs build code. Mirrors the same two-job split the deck recommends for release pipelines. --- .github/workflows/build-pdf.yml | 46 ++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index d2906d9..02c48f6 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -1,6 +1,10 @@ name: Build Presentation PDFs on: + pull_request: + paths: + - 'security-in-age-of-ai/**' + - '.github/workflows/build-pdf.yml' push: branches: [main] paths: @@ -12,15 +16,18 @@ on: permissions: {} jobs: + # ── Build ──────────────────────────────────────────────────────── + # Runs on every PR and push. No write access, no secrets — if a + # malicious PR ran during this job, it would find nothing useful. build: runs-on: ubuntu-latest permissions: - contents: write # commit the generated PDF back to the branch + contents: read steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - persist-credentials: true # needed for the commit-back step + persist-credentials: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -52,15 +59,36 @@ jobs: http://localhost:8000/security-in-age-of-ai/index.html \ security-in-age-of-ai/security-in-age-of-ai.pdf - - name: Commit PDF back to branch - # Skips the commit if there are no changes - uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0 - with: - commit_message: "chore(pdf): rebuild presentation PDFs" - file_pattern: "security-in-age-of-ai/*.pdf" - - name: Upload PDF as workflow artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: security-in-age-of-ai-pdf path: security-in-age-of-ai/security-in-age-of-ai.pdf + + # ── Publish ────────────────────────────────────────────────────── + # Only on push to main: consume the verified artifact and commit it + # back to the repo. Never sees source or runs build code itself. + publish: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: true # commit-back step needs the token + + - name: Download PDF artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: security-in-age-of-ai-pdf + path: security-in-age-of-ai/ + + - name: Commit PDF back to main + # Skips the commit if there are no changes + uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0 + with: + commit_message: "chore(pdf): rebuild presentation PDFs" + file_pattern: "security-in-age-of-ai/*.pdf" From af1fc0381f8562a1c0cb954d0020eb50f5ec0210 Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 11:22:39 -0700 Subject: [PATCH 06/12] Fix pnpm global bin dir for CI Set PNPM_HOME and add it to PATH so 'pnpm add -g' has a valid global bin directory. Without this, pnpm errors with ERR_PNPM_NO_GLOBAL_BIN_DIR. --- .github/workflows/build-pdf.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index 02c48f6..accd8ff 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -38,6 +38,12 @@ jobs: run: | corepack enable corepack prepare pnpm@9.15.0 --activate + # pnpm needs an explicit global bin dir on PATH for `pnpm add -g` + PNPM_HOME="$HOME/.local/share/pnpm" + mkdir -p "$PNPM_HOME" + pnpm config set global-bin-dir "$PNPM_HOME" + echo "PNPM_HOME=$PNPM_HOME" >> "$GITHUB_ENV" + echo "$PNPM_HOME" >> "$GITHUB_PATH" pnpm --version - name: Install decktape From 0ab31cc4e203a06dd07157093465338f2e2f6465 Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 11:24:08 -0700 Subject: [PATCH 07/12] Pass --no-sandbox to Chrome for headless run on GitHub runners GitHub-hosted Ubuntu runners can't use Chrome's SUID sandbox, so puppeteer fails with 'No usable sandbox'. Disabling it is safe inside the ephemeral runner. --- .github/workflows/build-pdf.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index accd8ff..ca698b2 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -58,8 +58,11 @@ jobs: run: pnpm dlx wait-on http://localhost:8000 - name: Build security-in-age-of-ai PDF + # --chrome-arg=--no-sandbox: GitHub-hosted Ubuntu runners can't use + # Chrome's SUID sandbox; safe inside this throwaway runner. run: | decktape reveal \ + --chrome-arg=--no-sandbox \ --size 1280x720 \ --load-pause 500 \ http://localhost:8000/security-in-age-of-ai/index.html \ From 68ed2c3cfd4212063e01e29a09a9cefe55ef1203 Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 11:42:54 -0700 Subject: [PATCH 08/12] Publish PDF to Zenodo instead of committing it back - New scripts/zenodo_publish.py uploads the PDF to the uw-ssec community on zenodo.org (or sandbox.zenodo.org when ZENODO_SANDBOX is set). First run creates a new deposition; later runs open a new version of the existing record and replace the file. Existing records are discovered by searching the authenticated user's depositions for a unique 'uw-ssec-deck:' keyword tag. - Per-deck metadata lives in /zenodo.json so adding another deck is just adding a JSON file plus a publish step. - Workflow publish job no longer commits; runs the script with ZENODO_TOKEN from secrets, defaults to sandbox so production records aren't created until ZENODO_SANDBOX repo var is set to 'false'. - Skips publish when the existing record's version already matches github.sha. --- .github/workflows/build-pdf.yml | 29 ++-- scripts/requirements.txt | 1 + scripts/zenodo_publish.py | 228 ++++++++++++++++++++++++++++++ security-in-age-of-ai/zenodo.json | 21 +++ 4 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 scripts/requirements.txt create mode 100644 scripts/zenodo_publish.py create mode 100644 security-in-age-of-ai/zenodo.json diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index ca698b2..5bcdc1f 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -75,19 +75,28 @@ jobs: path: security-in-age-of-ai/security-in-age-of-ai.pdf # ── Publish ────────────────────────────────────────────────────── - # Only on push to main: consume the verified artifact and commit it - # back to the repo. Never sees source or runs build code itself. + # Only on push to main: deposit the verified artifact to Zenodo + # under the uw-ssec community. First run creates a new record; + # later runs publish a new version of the existing record. publish: if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: build runs-on: ubuntu-latest permissions: - contents: write + contents: read steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - persist-credentials: true # commit-back step needs the token + persist-credentials: false + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.12' + + - name: Install Python dependencies + run: pip install -r scripts/requirements.txt - name: Download PDF artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -95,9 +104,9 @@ jobs: name: security-in-age-of-ai-pdf path: security-in-age-of-ai/ - - name: Commit PDF back to main - # Skips the commit if there are no changes - uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0 - with: - commit_message: "chore(pdf): rebuild presentation PDFs" - file_pattern: "security-in-age-of-ai/*.pdf" + - name: Publish to Zenodo (uw-ssec community) + env: + ZENODO_TOKEN: ${{ secrets.ZENODO_TOKEN }} + ZENODO_SANDBOX: ${{ vars.ZENODO_SANDBOX || 'true' }} + GIT_COMMIT_SHA: ${{ github.sha }} + run: python scripts/zenodo_publish.py security-in-age-of-ai diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..d80d9fc --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1 @@ +requests==2.32.3 diff --git a/scripts/zenodo_publish.py b/scripts/zenodo_publish.py new file mode 100644 index 0000000..1d5d5c1 --- /dev/null +++ b/scripts/zenodo_publish.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Publish a presentation PDF to Zenodo. + +On first run, creates a new deposition in the ``uw-ssec`` community. +On subsequent runs, opens a new version of the existing record and +replaces its file. The existing record is found by searching the +authenticated user's depositions for a unique keyword tag of the form +``uw-ssec-deck:``. + +Driven by a per-deck ``zenodo.json`` metadata file next to the deck +sources. Run as:: + + python scripts/zenodo_publish.py + +Required environment: + ZENODO_TOKEN Personal access token with ``deposit:write`` and + ``deposit:actions`` scopes. + +Optional environment: + ZENODO_SANDBOX When set to ``true``/``1``, target + sandbox.zenodo.org instead of zenodo.org. + GIT_COMMIT_SHA Recorded as the ``version`` field; the script + skips publishing when a record's most recent + version already matches this SHA. +""" +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path + +import requests + + +COMMUNITY = "uw-ssec" +KEYWORD_PREFIX = "uw-ssec-deck" + + +def env(name: str, default: str | None = None) -> str | None: + value = os.environ.get(name, default) + if value is not None and value != "": + return value + return default + + +def required_env(name: str) -> str: + value = env(name) + if not value: + sys.exit(f"missing required env var: {name}") + return value + + +def truthy(value: str | None) -> bool: + return (value or "").lower() in {"1", "true", "yes", "on"} + + +class Zenodo: + def __init__(self, token: str, sandbox: bool = False) -> None: + host = "sandbox.zenodo.org" if sandbox else "zenodo.org" + self.api = f"https://{host}/api" + self.session = requests.Session() + self.session.headers["Authorization"] = f"Bearer {token}" + + def _call(self, method: str, url: str, **kwargs) -> requests.Response: + kwargs.setdefault("timeout", 60) + r = self.session.request(method, url, **kwargs) + if not r.ok: + sys.stderr.write( + f"\nZenodo {method} {url} failed: {r.status_code}\n{r.text}\n" + ) + r.raise_for_status() + return r + + def find_existing(self, tag: str) -> dict | None: + """Return the newest deposition matching the keyword tag, or None.""" + r = self._call( + "GET", + f"{self.api}/deposit/depositions", + params={"q": f'keywords:"{tag}"', "size": 100, "sort": "mostrecent"}, + ) + matches = r.json() + return matches[0] if matches else None + + def new_record(self) -> dict: + r = self._call("POST", f"{self.api}/deposit/depositions", json={}) + return r.json() + + def new_version(self, deposit_id: int) -> dict: + """Open a new draft version of a published record.""" + r = self._call( + "POST", + f"{self.api}/deposit/depositions/{deposit_id}/actions/newversion", + ) + latest_draft_url = r.json()["links"]["latest_draft"] + return self._call("GET", latest_draft_url).json() + + def replace_files(self, deposit: dict, file_path: Path) -> None: + for f in deposit.get("files", []): + self._call( + "DELETE", + f"{self.api}/deposit/depositions/{deposit['id']}/files/{f['id']}", + ) + bucket = deposit["links"]["bucket"] + with file_path.open("rb") as fh: + self._call( + "PUT", + f"{bucket}/{file_path.name}", + data=fh, + timeout=600, + ) + + def set_metadata(self, deposit_id: int, metadata: dict) -> dict: + r = self._call( + "PUT", + f"{self.api}/deposit/depositions/{deposit_id}", + json={"metadata": metadata}, + headers={"Content-Type": "application/json"}, + ) + return r.json() + + def publish(self, deposit_id: int) -> dict: + r = self._call( + "POST", + f"{self.api}/deposit/depositions/{deposit_id}/actions/publish", + ) + return r.json() + + +def load_deck_metadata(deck_dir: Path) -> dict: + cfg_path = deck_dir / "zenodo.json" + if not cfg_path.is_file(): + sys.exit(f"missing {cfg_path}") + return json.loads(cfg_path.read_text()) + + +def build_metadata(deck: dict, slug: str, version: str | None) -> dict: + keywords = list(deck.get("keywords", [])) + tag = f"{KEYWORD_PREFIX}:{slug}" + if tag not in keywords: + keywords.append(tag) + + metadata: dict = { + "upload_type": deck.get("upload_type", "presentation"), + "title": deck["title"], + "description": deck["description"], + "creators": deck["creators"], + "communities": [{"identifier": COMMUNITY}], + "keywords": keywords, + "access_right": "open", + "license": deck.get("license", "cc-by-4.0"), + } + if "publication_date" in deck: + metadata["publication_date"] = deck["publication_date"] + if version: + metadata["version"] = version + return metadata + + +def already_published_at_sha(existing: dict, sha: str | None) -> bool: + if not sha: + return False + return (existing.get("metadata") or {}).get("version") == sha + + +def main(argv: list[str]) -> int: + if len(argv) != 2: + sys.exit("usage: zenodo_publish.py ") + + deck_dir = Path(argv[1]).resolve() + if not deck_dir.is_dir(): + sys.exit(f"not a directory: {deck_dir}") + + token = required_env("ZENODO_TOKEN") + sandbox = truthy(env("ZENODO_SANDBOX")) + commit_sha = env("GIT_COMMIT_SHA") + + deck = load_deck_metadata(deck_dir) + slug = deck.get("slug") or deck_dir.name + pdf_name = deck.get("pdf") or f"{slug}.pdf" + pdf_path = deck_dir / pdf_name + if not pdf_path.is_file(): + sys.exit(f"PDF not found: {pdf_path}") + + zen = Zenodo(token, sandbox=sandbox) + tag = f"{KEYWORD_PREFIX}:{slug}" + existing = zen.find_existing(tag) + + if existing and already_published_at_sha(existing, commit_sha): + print( + f"existing record {existing['id']} already at commit {commit_sha}; " + "nothing to do" + ) + return 0 + + metadata = build_metadata(deck, slug, commit_sha) + + if existing is None: + print(f"no existing record for {tag}; creating new deposition") + deposit = zen.new_record() + else: + print( + f"found existing deposition {existing['id']}; opening new version" + ) + deposit = zen.new_version(existing["id"]) + + deposit = zen.set_metadata(deposit["id"], metadata) + zen.replace_files(deposit, pdf_path) + published = zen.publish(deposit["id"]) + + record_url = published.get("links", {}).get("record_html") or published.get( + "links", {} + ).get("html") + doi = published.get("doi") or published.get("metadata", {}).get("doi") + print(f"published: {record_url}") + print(f"DOI: {doi}") + + summary = os.environ.get("GITHUB_STEP_SUMMARY") + if summary: + with open(summary, "a", encoding="utf-8") as fh: + fh.write(f"## Zenodo publish — {deck['title']}\n\n") + fh.write(f"- Record: <{record_url}>\n") + fh.write(f"- DOI: `{doi}`\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/security-in-age-of-ai/zenodo.json b/security-in-age-of-ai/zenodo.json new file mode 100644 index 0000000..23cf33d --- /dev/null +++ b/security-in-age-of-ai/zenodo.json @@ -0,0 +1,21 @@ +{ + "slug": "security-in-age-of-ai", + "title": "Security in the Age of AI", + "upload_type": "presentation", + "description": "

Open Source Supply Chain Security: Threats, Mitigations & Hardened Workflows.

Slides from the UW Scientific Software Engineering Center (SSEC) Research Software Engineering Meetup. Covers GitHub Actions attack classes, recent supply-chain incidents (tj-actions, Shai-Hulud, Ultralytics, Copy Fail), the convergence of frontier AI and security disclosure, and hardened consumer and publisher CI/CD workflows for npm and PyPI.

", + "creators": [ + {"name": "Setiawan, Landung", "affiliation": "University of Washington Scientific Software Engineering Center"}, + {"name": "Core, Cordero", "affiliation": "University of Washington Scientific Software Engineering Center"} + ], + "keywords": [ + "security", + "supply-chain", + "github-actions", + "ai", + "open-source", + "ci-cd", + "research-software-engineering" + ], + "license": "cc-by-4.0", + "pdf": "security-in-age-of-ai.pdf" +} From b9c01be752f530dc9f2f6d3a1c288be8976f6d0f Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 11:47:57 -0700 Subject: [PATCH 09/12] Use CalVer (YYYY.MM.DD) for Zenodo version field Replaces the git-SHA version with a calendar version computed at publish time in UTC. When more than one publish happens on the same day, the version becomes YYYY.MM.DD.N with N incremented from the previous record's version. --- .github/workflows/build-pdf.yml | 1 - scripts/zenodo_publish.py | 44 +++++++++++++++++++-------------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index 5bcdc1f..da14c9f 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -108,5 +108,4 @@ jobs: env: ZENODO_TOKEN: ${{ secrets.ZENODO_TOKEN }} ZENODO_SANDBOX: ${{ vars.ZENODO_SANDBOX || 'true' }} - GIT_COMMIT_SHA: ${{ github.sha }} run: python scripts/zenodo_publish.py security-in-age-of-ai diff --git a/scripts/zenodo_publish.py b/scripts/zenodo_publish.py index 1d5d5c1..ba3a957 100644 --- a/scripts/zenodo_publish.py +++ b/scripts/zenodo_publish.py @@ -19,12 +19,14 @@ Optional environment: ZENODO_SANDBOX When set to ``true``/``1``, target sandbox.zenodo.org instead of zenodo.org. - GIT_COMMIT_SHA Recorded as the ``version`` field; the script - skips publishing when a record's most recent - version already matches this SHA. + +Versioning is CalVer (``YYYY.MM.DD``, UTC). When more than one publish +happens on the same day, the version becomes ``YYYY.MM.DD.N`` with +``N`` incremented from the previous record's version. """ from __future__ import annotations +import datetime import json import os import sys @@ -134,6 +136,24 @@ def load_deck_metadata(deck_dir: Path) -> dict: return json.loads(cfg_path.read_text()) +def next_calver(existing: dict | None) -> str: + """Compute today's CalVer, bumping ``.N`` if it collides with the + previous record's version (e.g. multiple publishes per day).""" + today = datetime.datetime.now(datetime.timezone.utc).strftime("%Y.%m.%d") + if existing is None: + return today + prev = ((existing.get("metadata") or {}).get("version") or "").strip() + if prev == today: + return f"{today}.2" + if prev.startswith(f"{today}."): + try: + n = int(prev.rsplit(".", 1)[-1]) + except ValueError: + return f"{today}.2" + return f"{today}.{n + 1}" + return today + + def build_metadata(deck: dict, slug: str, version: str | None) -> dict: keywords = list(deck.get("keywords", [])) tag = f"{KEYWORD_PREFIX}:{slug}" @@ -157,12 +177,6 @@ def build_metadata(deck: dict, slug: str, version: str | None) -> dict: return metadata -def already_published_at_sha(existing: dict, sha: str | None) -> bool: - if not sha: - return False - return (existing.get("metadata") or {}).get("version") == sha - - def main(argv: list[str]) -> int: if len(argv) != 2: sys.exit("usage: zenodo_publish.py ") @@ -173,7 +187,6 @@ def main(argv: list[str]) -> int: token = required_env("ZENODO_TOKEN") sandbox = truthy(env("ZENODO_SANDBOX")) - commit_sha = env("GIT_COMMIT_SHA") deck = load_deck_metadata(deck_dir) slug = deck.get("slug") or deck_dir.name @@ -186,14 +199,9 @@ def main(argv: list[str]) -> int: tag = f"{KEYWORD_PREFIX}:{slug}" existing = zen.find_existing(tag) - if existing and already_published_at_sha(existing, commit_sha): - print( - f"existing record {existing['id']} already at commit {commit_sha}; " - "nothing to do" - ) - return 0 - - metadata = build_metadata(deck, slug, commit_sha) + version = next_calver(existing) + print(f"version: {version}") + metadata = build_metadata(deck, slug, version) if existing is None: print(f"no existing record for {tag}; creating new deposition") From a3b6fe74e3987454142be9b0746844496d52185b Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 11:51:34 -0700 Subject: [PATCH 10/12] =?UTF-8?q?Route=20publish=20target=20by=20branch:?= =?UTF-8?q?=20staging=E2=86=92sandbox,=20main=E2=86=92production?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Workflow now also triggers on push to 'staging'. - The publish job runs on either main or staging, but selects the Zenodo host and token based on which branch fired: staging → sandbox.zenodo.org via ZENODO_SANDBOX_TOKEN main → zenodo.org via ZENODO_TOKEN - Drops the vars.ZENODO_SANDBOX override; branch decides. --- .github/workflows/build-pdf.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index da14c9f..6518883 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -6,7 +6,7 @@ on: - 'security-in-age-of-ai/**' - '.github/workflows/build-pdf.yml' push: - branches: [main] + branches: [main, staging] paths: - 'security-in-age-of-ai/**' - '.github/workflows/build-pdf.yml' @@ -75,11 +75,12 @@ jobs: path: security-in-age-of-ai/security-in-age-of-ai.pdf # ── Publish ────────────────────────────────────────────────────── - # Only on push to main: deposit the verified artifact to Zenodo - # under the uw-ssec community. First run creates a new record; - # later runs publish a new version of the existing record. + # Push to main → production zenodo.org + # Push to staging → sandbox.zenodo.org (for testing the full flow) + # First run on each target creates a new record; later runs publish + # a new version of the existing record. publish: - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') needs: build runs-on: ubuntu-latest permissions: @@ -106,6 +107,8 @@ jobs: - name: Publish to Zenodo (uw-ssec community) env: - ZENODO_TOKEN: ${{ secrets.ZENODO_TOKEN }} - ZENODO_SANDBOX: ${{ vars.ZENODO_SANDBOX || 'true' }} + # staging → sandbox, main → production. Each Zenodo host has + # its own account and its own token. + ZENODO_TOKEN: ${{ github.ref == 'refs/heads/staging' && secrets.ZENODO_SANDBOX_TOKEN || secrets.ZENODO_TOKEN }} + ZENODO_SANDBOX: ${{ github.ref == 'refs/heads/staging' && 'true' || 'false' }} run: python scripts/zenodo_publish.py security-in-age-of-ai From 43dac0d96dc94e35f549df57e0906de9873034d2 Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 12:00:25 -0700 Subject: [PATCH 11/12] Move Zenodo community into per-deck zenodo.json Lets each deck pick its own community without editing the script. Defaults to 'uw-ssec' when unset. --- scripts/zenodo_publish.py | 5 +++-- security-in-age-of-ai/zenodo.json | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/zenodo_publish.py b/scripts/zenodo_publish.py index ba3a957..16b28b3 100644 --- a/scripts/zenodo_publish.py +++ b/scripts/zenodo_publish.py @@ -35,7 +35,7 @@ import requests -COMMUNITY = "uw-ssec" +DEFAULT_COMMUNITY = "uw-ssec" KEYWORD_PREFIX = "uw-ssec-deck" @@ -160,12 +160,13 @@ def build_metadata(deck: dict, slug: str, version: str | None) -> dict: if tag not in keywords: keywords.append(tag) + community = deck.get("community", DEFAULT_COMMUNITY) metadata: dict = { "upload_type": deck.get("upload_type", "presentation"), "title": deck["title"], "description": deck["description"], "creators": deck["creators"], - "communities": [{"identifier": COMMUNITY}], + "communities": [{"identifier": community}], "keywords": keywords, "access_right": "open", "license": deck.get("license", "cc-by-4.0"), diff --git a/security-in-age-of-ai/zenodo.json b/security-in-age-of-ai/zenodo.json index 23cf33d..a5ee704 100644 --- a/security-in-age-of-ai/zenodo.json +++ b/security-in-age-of-ai/zenodo.json @@ -1,5 +1,6 @@ { "slug": "security-in-age-of-ai", + "community": "uw-ssec", "title": "Security in the Age of AI", "upload_type": "presentation", "description": "

Open Source Supply Chain Security: Threats, Mitigations & Hardened Workflows.

Slides from the UW Scientific Software Engineering Center (SSEC) Research Software Engineering Meetup. Covers GitHub Actions attack classes, recent supply-chain incidents (tj-actions, Shai-Hulud, Ultralytics, Copy Fail), the convergence of frontier AI and security disclosure, and hardened consumer and publisher CI/CD workflows for npm and PyPI.

", From dbaddb8cc84f9c2c2945ea49c99c0ddb38fa6499 Mon Sep 17 00:00:00 2001 From: Landung 'Don' Setiawan Date: Fri, 15 May 2026 12:47:03 -0700 Subject: [PATCH 12/12] Add ORCID iDs for Don and Cordero in zenodo metadata --- security-in-age-of-ai/zenodo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/security-in-age-of-ai/zenodo.json b/security-in-age-of-ai/zenodo.json index a5ee704..a4afab7 100644 --- a/security-in-age-of-ai/zenodo.json +++ b/security-in-age-of-ai/zenodo.json @@ -5,8 +5,8 @@ "upload_type": "presentation", "description": "

Open Source Supply Chain Security: Threats, Mitigations & Hardened Workflows.

Slides from the UW Scientific Software Engineering Center (SSEC) Research Software Engineering Meetup. Covers GitHub Actions attack classes, recent supply-chain incidents (tj-actions, Shai-Hulud, Ultralytics, Copy Fail), the convergence of frontier AI and security disclosure, and hardened consumer and publisher CI/CD workflows for npm and PyPI.

", "creators": [ - {"name": "Setiawan, Landung", "affiliation": "University of Washington Scientific Software Engineering Center"}, - {"name": "Core, Cordero", "affiliation": "University of Washington Scientific Software Engineering Center"} + {"name": "Setiawan, Landung", "affiliation": "University of Washington Scientific Software Engineering Center", "orcid": "0000-0002-1624-2667"}, + {"name": "Core, Cordero", "affiliation": "University of Washington Scientific Software Engineering Center", "orcid": "0000-0002-3531-3221"} ], "keywords": [ "security",