From a68bca04873966086eff8bbe59d3e1f28c79d3f0 Mon Sep 17 00:00:00 2001 From: vsoch Date: Thu, 3 Mar 2022 23:58:31 -0700 Subject: [PATCH] adding workflow preparing to test Signed-off-by: vsoch --- .github/workflows/release.yaml | 28 ++++++ .gitignore | 1 + .zenodo.json | 4 +- README.md | 66 +++++++++++++- action.yml | 37 ++++++-- scripts/deploy.py | 154 +++++++++++++++++++++++++++++++++ 6 files changed, 283 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/release.yaml create mode 100644 .gitignore create mode 100644 scripts/deploy.py diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..040d278 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,28 @@ +name: Zenodo Release + +on: + release: + types: [published] + +jobs: + deploy: + runs-on: ubuntu-20.04 + + steps: + + - uses: actions/checkout@v3 + - name: download archive to runner + env: + zipball: ${{ github.event.release.zipball_url }} + tarball: ${{ github.event.release.tarball_url }} + version: ${{ github.event.release.tag_name }} + run: | + name=$(basename ${tarball}) + curl -L $tarball > $name + echo "archive=${name}" >> $GITHUB_ENV + + - name: Run Zenodo Deploy + with: + version: ${{ github.event.release.tag_name }} + zenodo_json: .zenodo.json + archive: ${{ env.archive }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/.zenodo.json b/.zenodo.json index 1712b1b..414be5f 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -11,5 +11,7 @@ "affiliation": "Ohio SuperComputer Center", "name": "Jeff Ohrstrom" } - ] + ], + "keywords": ["zenodo", "release", "archive"], + "license": "MIT" } diff --git a/README.md b/README.md index 9581268..3a3bd32 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,70 @@ on release, and without needing to enable admin webhooks. To get this working yo 1. Create an account on Zenodo 2. Under your name -> Applications -> Developer Applications -> Personal Access Tokens -> +New Token and choose both scopes for deposit 3. Add the token to your repository secrets as `ZENODO_TOKEN` +4. Create a .zenodo.json file for the root of your repository (see [template](.zenodo.json)) +5. Add the example action (modified for your release) to your GitHub repository. + +**Important** You CANNOT create a release online first and then try to upload to the same DOI. +If you do this, you'll get: -**todo** under development! +```python +{'status': 400, + 'message': 'Validation error.', + 'errors': [{'field': 'metadata.doi', + 'message': 'The prefix 10.5281 is administrated locally.'}]} +``` + +I think this is kind of silly, but that's just me. + +## Usage + +### GitHub Action + +After you complete the steps above to create the metadata file, you might create a release +action as follows: + +```yaml +name: Zenodo Release + +on: + release: + types: [published] + +jobs: + deploy: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v3 + - name: download archive to runner + env: + tarball: ${{ github.event.release.tarball_url }} + run: | + name=$(basename ${tarball}) + curl -L $tarball > $name + echo "archive=${name}" >> $GITHUB_ENV + + - name: Run Zenodo Deploy + with: + version: ${{ github.event.release.tag_name }} + zenodo_json: .zenodo.json + archive: ${{ env.archive }} +``` + +Notice how we are choosing to use the .tar.gz (you could use the zip too at `${{ github.event.release.zipball_url }}`) +and using the default zenodo.json that is obtained from the checked out repository. +We also grab the version as the release tag. We are also running on the publication of a release. + +### Local + +If you want to use the script locally (meaning to manually push a release) you can wget +or download the release (usually .tar.gz or .zip) and then export your zenodo token and do +the following: + +```bash +export ZENODO_TOKEN=xxxxxxxxxxxxxxxxxxxx + + # archive # identifier # version +$ python scripts/deploy.py upload 0.0.0.tar.gz 6326700 --version 0.0.0 +``` diff --git a/action.yml b/action.yml index 3d31929..301f227 100644 --- a/action.yml +++ b/action.yml @@ -10,20 +10,47 @@ inputs: zenodo_json: description: Path to zenodo.json to upload with metadata (must exist) -#outputs: -# matrix: -# description: matrix of spliced builds -# value: ${{ steps.matrix.outputs.matrix }} +outputs: + badge: + description: badge url + value: ${{ steps.deploy.outputs.badge }} + bucket: + description: bucket url + value: ${{ steps.deploy.outputs.bucket }} + conceptbadge: + description: concept badge url + value: ${{ steps.deploy.outputs.conceptbadge }} + conceptdoi: + description: concept doi url + value: ${{ steps.deploy.outputs.conceptdoi }} + doi: + description: doi url + value: ${{ steps.deploy.outputs.doi }} + latest: + description: latest url + value: ${{ steps.deploy.outputs.latest }} + latest_html: + description: latest html url + value: ${{ steps.deploy.outputs.latest_html }} + record: + description: record url + value: ${{ steps.deploy.outputs.record }} + record_html: + description: record html url + value: ${{ steps.deploy.outputs.record_html }} runs: using: "composite" steps: - name: Deploy Zenodo + id: deploy env: zenodo_json: ${{ inputs.zenodo_json }} archive: ${{ inputs.archive }} version: ${{ inputs.version }} ACTION_PATH: ${{ github.action_path }} - run: ${{ github.action_path }}/scripts/deploy.py ${zenodo_json} ${archive} --version ${version} + run: | + pip install requests + ${{ github.action_path }}/scripts/deploy.py upload ${archive} --zenodo-json ${zenodo_json} --version ${version} shell: python diff --git a/scripts/deploy.py b/scripts/deploy.py new file mode 100644 index 0000000..92e239e --- /dev/null +++ b/scripts/deploy.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 + +# This script does the following. +# 1. Takes in a space separated list of changed files +# 2. For each changed file, adds a header (title) based on the filename +# 3. Sets output for the prepared files to move into the site + + +import argparse +import os +import json +import sys +from datetime import datetime +import requests + + +def read_file(filename): + with open(filename, "r") as fd: + content = fd.read() + return content + + +def read_json(filename): + with open(filename, "r") as fd: + content = json.loads(fd.read()) + return content + + +ZENODO_TOKEN = os.environ.get("ZENODO_TOKEN") +ZENODO_HOST = "zenodo.org" +if not ZENODO_TOKEN: + sys.exit("A ZENODO_TOKEN is required to be exported in the environment!") + + +def upload_archive(archive, zenodo_json, version): + """ + Upload an archive to zenodo + """ + archive = os.path.abspath(archive) + if not os.path.exists(archive): + sys.exit("Archive %s does not exist." % archive) + + headers = {"Accept": "application/json"} + params = {"access_token": ZENODO_TOKEN} + + # Create an empty upload + response = requests.post( + "https://zenodo.org/api/deposit/depositions", + params=params, + json={}, + headers=headers, + ) + if response.status_code != 200: + sys.exit( + "Trouble requesting new upload: %s, %s" + % (response.status_code, response.json()) + ) + + upload = response.json() + + # Using requests files indicates multipart/form-data + # Here we are uploading the new release file + url = "https://zenodo.org/api/deposit/depositions/%s/files" % upload["id"] + bucket_url = upload["links"]["bucket"] + + with open(archive, "rb") as fp: + response = requests.put( + "%s/%s" % (bucket_url, os.path.basename(archive)), + data=fp, + params=params, + ) + if response.status_code != 200: + sys.exit("Trouble uploading artifact %s to bucket" % archive) + + # Finally, load .zenodo.json and add version + metadata = read_json(zenodo_json) + metadata["version"] = version + metadata["publication_date"] = str(datetime.now()) + if "upload_type" not in metadata: + metadata["upload_type"] = "software" + url = "https://zenodo.org/api/deposit/depositions/%s" % upload["id"] + headers["Content-Type"] = "application/json" + response = requests.put( + url, data=json.dumps({"metadata": metadata}), params=params, headers=headers + ) + if response.status_code != 200: + sys.exit( + "Trouble uploading metadata %s, %s" % response.status_code, response.json() + ) + + data = response.json() + publish_url = data["links"]["publish"] + r = requests.post(publish_url, params=params) + if r.status_code not in [200, 201, 202]: + sys.exit( + "Issue publishing record: %s, %s" % (response.status_code, response.json()) + ) + + published = r.json() + print("::group::Record") + print(json.dumps(published, indent=4)) + print("::endgroup::") + for k, v in published["links"].items(): + print("::set-output name=%s::%s" % (k, v)) + + +def get_parser(): + parser = argparse.ArgumentParser(description="Zenodo Uploader") + subparsers = parser.add_subparsers( + help="actions", + title="actions", + description="Upload to Zenodo", + dest="command", + ) + upload = subparsers.add_parser("upload", help="upload an archive to zenodo") + upload.add_argument("archive", help="archive to upload") + upload.add_argument( + "--zenodo-json", + dest="zenodo_json", + help="path to .zenodo.json (defaults to .zenodo.json)", + default=".zenodo.json", + ) + upload.add_argument("--version", help="version to upload") + return parser + + +def main(): + parser = get_parser() + + def help(return_code=0): + parser.print_help() + sys.exit(return_code) + + # If an error occurs while parsing the arguments, the interpreter will exit with value 2 + args, extra = parser.parse_known_args() + if not args.command: + help() + + if not args.zenodo_json or not os.path.exists(args.zenodo_json): + sys.exit( + "You must provide an existing .zenodo.json as the first positional argument." + ) + if not args.archive: + sys.exit("You must provide an archive as the second positional argument.") + if not args.version: + sys.exit("You must provide a software version to upload.") + + # Prepare drafts + if args.command == "upload": + upload_archive(args.archive, args.zenodo_json, args.version) + + +if __name__ == "__main__": + main()