Skip to content

Commit 3a950a3

Browse files
committed
added missing action
1 parent 8de3fc8 commit 3a950a3

File tree

3 files changed

+290
-0
lines changed

3 files changed

+290
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: 'Create zenodo release'
2+
description: 'Create release and deploy tool to Zenodo'
3+
inputs:
4+
tool:
5+
required: true
6+
prevrecord:
7+
required: true
8+
token:
9+
required: true
10+
runs:
11+
using: "composite"
12+
steps:
13+
- name: Set up Python
14+
uses: actions/setup-python@v5
15+
with:
16+
python-version: '3.11'
17+
18+
- name: Install Python dependencies
19+
run: pip install -r ${{github.action_path}}/requirements.txt
20+
shell: bash
21+
22+
- name: Create Zenodo release
23+
run: |
24+
python3 ${{github.action_path}}/zenodo_release.py \
25+
--token '${{inputs.token}}' \
26+
--record-id '${{inputs.prevrecord}}' \
27+
--tool '${{inputs.tool}}' \
28+
--file 'upload/${{inputs.tool}}.zip' \
29+
--metadata '${{github.action_path}}/metadata.json' \
30+
--cleanup-drafts
31+
shell: bash
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"metadata": {
3+
"title": "__TOOL__ - Verifier Archive",
4+
"publication_date": "__TODAY__",
5+
"access_right": "open",
6+
"creators": [
7+
{
8+
"name": "\u00c1d\u00e1m, Zs\u00f3fia",
9+
"affiliation": "Budapest University of Technology and Economics",
10+
"orcid": "0000-0003-2354-1750"
11+
},
12+
{
13+
"name": "Bajczi, Levente",
14+
"affiliation": "Budapest University of Technology and Economics",
15+
"orcid": "0000-0002-6551-5860"
16+
}
17+
],
18+
"custom": {
19+
"code:codeRepository": "https://github.com/ftsrg/ConcurrentWitness2Test",
20+
"code:programmingLanguage": [
21+
{
22+
"id": "python",
23+
"title": {
24+
"en": "Python"
25+
}
26+
}
27+
]
28+
},
29+
"license": "apache2.0",
30+
"imprint_publisher": "Zenodo",
31+
"upload_type": "software"
32+
}
33+
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Zenodo release automation script.
4+
Handles creating new versions, uploading files, and publishing to Zenodo.
5+
"""
6+
7+
import argparse
8+
import json
9+
import os
10+
import sys
11+
from datetime import date
12+
from pathlib import Path
13+
from typing import Optional, Dict, Any
14+
15+
import requests
16+
17+
18+
class ZenodoReleaser:
19+
def __init__(self, token: str, record_id: str):
20+
self.token = token
21+
self.record_id = record_id
22+
self.base_url = "https://zenodo.org/api"
23+
self.headers = {
24+
"Authorization": f"Bearer {token}",
25+
"Content-Type": "application/json"
26+
}
27+
28+
def _make_request(self, method: str, url: str, **kwargs) -> requests.Response:
29+
"""Make HTTP request with error handling."""
30+
try:
31+
response = requests.request(method, url, **kwargs)
32+
response.raise_for_status()
33+
return response
34+
except requests.exceptions.RequestException as e:
35+
print(f"Request failed: {e}")
36+
if hasattr(e.response, 'text'):
37+
print(f"Response: {e.response.text}")
38+
raise
39+
40+
def get_record_info(self) -> Dict[str, Any]:
41+
"""Get information about the record."""
42+
url = f"{self.base_url}/records/{self.record_id}"
43+
response = self._make_request("GET", url)
44+
return response.json()
45+
46+
def get_draft_depositions(self) -> list:
47+
"""Get all draft depositions for this record."""
48+
url = f"{self.base_url}/deposit/depositions"
49+
params = {"q": f"conceptrecid:{self.record_id}", "all_versions": "true"}
50+
response = self._make_request("GET", url, headers=self.headers, params=params)
51+
data = response.json()
52+
53+
# Filter for drafts only
54+
drafts = [d for d in data if d.get('submitted') is False]
55+
return drafts
56+
57+
def delete_draft(self, deposition_id: str) -> None:
58+
"""Delete a draft deposition."""
59+
url = f"{self.base_url}/deposit/depositions/{deposition_id}"
60+
print(f"Deleting draft deposition {deposition_id}...")
61+
try:
62+
self._make_request("DELETE", url, headers=self.headers)
63+
print(f"Successfully deleted draft {deposition_id}")
64+
except Exception as e:
65+
print(f"Warning: Could not delete draft {deposition_id}: {e}")
66+
67+
def cleanup_existing_drafts(self) -> None:
68+
"""Clean up any existing draft versions that might be in limbo."""
69+
print("Checking for existing draft versions...")
70+
drafts = self.get_draft_depositions()
71+
72+
if drafts:
73+
print(f"Found {len(drafts)} existing draft(s). Cleaning up...")
74+
for draft in drafts:
75+
draft_id = draft['id']
76+
self.delete_draft(draft_id)
77+
else:
78+
print("No existing drafts found.")
79+
80+
def create_new_version(self) -> Dict[str, Any]:
81+
"""Create a new version of the record."""
82+
print(f"Creating new version for record {self.record_id}")
83+
84+
url = f"{self.base_url}/deposit/depositions/{self.record_id}/actions/newversion"
85+
response = self._make_request("POST", url, headers=self.headers)
86+
data = response.json()
87+
88+
# Get the latest draft URL from the response
89+
latest_draft_url = data.get('links', {}).get('latest_draft')
90+
if not latest_draft_url:
91+
raise ValueError("Could not get latest draft URL from response")
92+
93+
# Fetch the draft to get the actual deposition data
94+
draft_response = self._make_request("GET", latest_draft_url, headers=self.headers)
95+
draft_data = draft_response.json()
96+
97+
return draft_data
98+
99+
def update_metadata(self, deposition_data: Dict[str, Any], tool_name: str, metadata_file: Path) -> None:
100+
"""Update the metadata for the deposition."""
101+
deposition_id = deposition_data['id']
102+
print(f"Updating metadata for deposition {deposition_id}")
103+
104+
# Read the metadata template
105+
with open(metadata_file, 'r') as f:
106+
metadata_template = json.load(f)
107+
108+
# Replace placeholders
109+
today = date.today().strftime('%Y-%m-%d')
110+
metadata_json = json.dumps(metadata_template)
111+
metadata_json = metadata_json.replace('__TODAY__', today)
112+
metadata_json = metadata_json.replace('__TOOL__', tool_name)
113+
metadata = json.loads(metadata_json)
114+
115+
# Update the deposition
116+
url = deposition_data['links']['self']
117+
response = self._make_request("PUT", url, headers=self.headers, json=metadata)
118+
119+
print(f"Metadata updated successfully (date := {today}, tool := {tool_name})")
120+
return response.json()
121+
122+
def delete_existing_files(self, deposition_data: Dict[str, Any]) -> None:
123+
"""Delete any existing files from the draft."""
124+
files = deposition_data.get('files', [])
125+
if not files:
126+
print("No existing files to delete.")
127+
return
128+
129+
print(f"Deleting {len(files)} existing file(s)...")
130+
for file_info in files:
131+
file_id = file_info['id']
132+
deposition_id = deposition_data['id']
133+
url = f"{self.base_url}/deposit/depositions/{deposition_id}/files/{file_id}"
134+
try:
135+
self._make_request("DELETE", url, headers=self.headers)
136+
print(f"Deleted file: {file_info['filename']}")
137+
except Exception as e:
138+
print(f"Warning: Could not delete file {file_info['filename']}: {e}")
139+
140+
def upload_file(self, deposition_data: Dict[str, Any], file_path: Path) -> None:
141+
"""Upload a file to the deposition."""
142+
bucket_url = deposition_data['links']['bucket']
143+
filename = file_path.name
144+
145+
print(f"Uploading file: {filename} ({file_path.stat().st_size / (1024*1024):.2f} MB)")
146+
147+
# Upload using the bucket API (streaming)
148+
upload_url = f"{bucket_url}/{filename}"
149+
with open(file_path, 'rb') as f:
150+
headers = {"Authorization": f"Bearer {self.token}"}
151+
response = self._make_request("PUT", upload_url, headers=headers, data=f)
152+
153+
print(f"Upload successful: {filename}")
154+
155+
def publish(self, deposition_data: Dict[str, Any]) -> Dict[str, Any]:
156+
"""Publish the deposition."""
157+
publish_url = deposition_data['links']['publish']
158+
print(f"Publishing deposition...")
159+
160+
response = self._make_request("POST", publish_url, headers=self.headers)
161+
data = response.json()
162+
163+
doi = data.get('doi', 'N/A')
164+
doi_url = data.get('doi_url', 'N/A')
165+
print(f"Publish successful!")
166+
print(f"DOI: {doi}")
167+
print(f"DOI URL: {doi_url}")
168+
169+
return data
170+
171+
172+
def main():
173+
parser = argparse.ArgumentParser(description='Create and publish a Zenodo release')
174+
parser.add_argument('--token', required=True, help='Zenodo API token')
175+
parser.add_argument('--record-id', required=True, help='Previous record ID')
176+
parser.add_argument('--tool', required=True, help='Tool name')
177+
parser.add_argument('--file', required=True, help='ZIP file to upload')
178+
parser.add_argument('--metadata', required=True, help='Metadata JSON file')
179+
parser.add_argument('--cleanup-drafts', action='store_true',
180+
help='Clean up existing draft versions before creating new one')
181+
182+
args = parser.parse_args()
183+
184+
# Validate file paths
185+
file_path = Path(args.file)
186+
if not file_path.exists():
187+
print(f"Error: File not found: {file_path}")
188+
sys.exit(1)
189+
190+
metadata_path = Path(args.metadata)
191+
if not metadata_path.exists():
192+
print(f"Error: Metadata file not found: {metadata_path}")
193+
sys.exit(1)
194+
195+
try:
196+
releaser = ZenodoReleaser(args.token, args.record_id)
197+
198+
# Optional: Clean up any existing drafts
199+
if args.cleanup_drafts:
200+
releaser.cleanup_existing_drafts()
201+
202+
# Create new version
203+
deposition = releaser.create_new_version()
204+
205+
# Update metadata
206+
deposition = releaser.update_metadata(deposition, args.tool, metadata_path)
207+
208+
# Delete any existing files from the draft
209+
releaser.delete_existing_files(deposition)
210+
211+
# Upload the file
212+
releaser.upload_file(deposition, file_path)
213+
214+
# Publish
215+
releaser.publish(deposition)
216+
217+
print("\n=== Release completed successfully! ===")
218+
219+
except Exception as e:
220+
print(f"\n=== Release failed ===")
221+
print(f"Error: {e}")
222+
sys.exit(1)
223+
224+
225+
if __name__ == '__main__':
226+
main()

0 commit comments

Comments
 (0)