├── .circleci └── config.yml ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── autofix.yml │ ├── automerge.yml │ ├── lint.yml │ └── status.yml ├── .gitignore ├── .yamllint.yml ├── LICENSE ├── README.md ├── action.yml ├── dist └── index.js ├── index.js ├── index.test.js ├── package-lock.json └── package.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:lts 6 | working_directory: ~/repo 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | keys: 11 | - v1-dependencies-{{ checksum "package.json" }} 12 | # fallback to using the latest cache if no exact match is found 13 | - v1-dependencies- 14 | - run: npm install 15 | - run: | 16 | mkdir ~/my_test_artifacts 17 | echo "This should point to root_artifact.md for commit ${CIRCLE_SHA1}" > ~/my_test_artifacts/root_artifact.md 18 | - save_cache: 19 | paths: 20 | - node_modules 21 | key: v1-dependencies-{{ checksum "package.json" }} 22 | - run: npm test 23 | - store_artifacts: 24 | path: ~/my_test_artifacts 25 | destination: test_artifacts 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2018 14 | }, 15 | "rules": { 16 | } 17 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: ".github/workflows" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | groups: 13 | actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci 2 | concurrency: 3 | group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }} 4 | cancel-in-progress: true 5 | 6 | on: # yamllint disable-line rule:truthy 7 | # We only do this on PRs to avoid the (admittedly unlikely) scenario that 8 | # we run, green, wait, merge, then the build on `main` could fail because conda 9 | # has updated during the "green" and then the "build on `main`" steps 10 | pull_request: 11 | branches: ["*"] 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | name: Rebuild dist 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.12" 22 | - run: npm install 23 | - run: npm i -g @vercel/ncc 24 | - run: ncc build index.js 25 | env: 26 | NODE_OPTIONS: "--openssl-legacy-provider" 27 | - uses: autofix-ci/action@2891949f3779a1cafafae1523058501de3d4e944 # v1.3.1 28 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | # Adapted from MNE-Python 2 | name: Bot auto-merge 3 | on: pull_request # yamllint disable-line rule:truthy 4 | 5 | jobs: 6 | autobot: 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | runs-on: ubuntu-latest 11 | # Names can be found with gh api /repos/mne-tools/mne-python/pulls/12998 -q .user.login for example 12 | if: (github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.user.login == 'pre-commit-ci[bot]' || github.event.pull_request.user.login == 'github-actions[bot]') && github.repository == 'scientific-python/circleci-artifacts-redirector-action' 13 | steps: 14 | - name: Enable auto-merge for bot PRs 15 | run: gh pr merge --auto --squash "$PR_URL" 16 | env: 17 | PR_URL: ${{github.event.pull_request.html_url}} 18 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 19 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint' 2 | concurrency: 3 | group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }} 4 | cancel-in-progress: true 5 | on: 6 | push: 7 | branches: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - '*' 12 | jobs: 13 | yaml_lint: 14 | name: YAML 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: ibiqlik/action-yamllint@v3 19 | with: 20 | file_or_dir: .github/workflows 21 | no_warnings: true 22 | config_file: .yamllint.yml 23 | -------------------------------------------------------------------------------- /.github/workflows/status.yml: -------------------------------------------------------------------------------- 1 | on: [status] 2 | jobs: 3 | circleci_artifacts_redirector_status_job: 4 | permissions: 5 | statuses: write 6 | runs-on: ubuntu-latest 7 | if: "${{ github.event.context == 'ci/circleci: build' }}" 8 | name: CircleCI artifacts redirector status 9 | outputs: 10 | url: ${{ steps.run-circleci-artifacts-redirector.outputs.url }} 11 | steps: 12 | # WARNING! 13 | # This repo uses the action in its own root directory. 14 | # GitHub-action users should not do this! 15 | # See README.md for usage instructions. 16 | - name: Print event data 17 | env: 18 | GITHUB_EVENT: ${{ toJSON(github.event) }} 19 | run: echo "$GITHUB_EVENT" 20 | - name: Print expected ref 21 | run: echo SHA=${{ github.event.sha }} 22 | - name: Checkout repo 23 | uses: actions/checkout@master 24 | with: 25 | ref: ${{ github.event.sha }} 26 | - name: Check history 27 | run: git log -n 1 28 | - name: GitHub Action step 29 | uses: ./ 30 | with: 31 | repo-token: ${{ secrets.GITHUB_TOKEN }} 32 | api-token: ${{ secrets.CIRCLECI_TOKEN }} 33 | artifact-path: 0/test_artifacts/root_artifact.md 34 | job-title: "See artifact here (status)!" 35 | id: run-circleci-artifacts-redirector 36 | 37 | use_outputs_status_job: 38 | runs-on: ubuntu-latest 39 | needs: [circleci_artifacts_redirector_status_job] 40 | name: Use the status output 41 | if: github.event.state != 'pending' 42 | steps: 43 | - name: Checkout repo 44 | uses: actions/checkout@master 45 | with: 46 | ref: ${{ github.event.sha }} 47 | - name: Get expected commit 48 | run: | 49 | echo ${{ needs.circleci_artifacts_redirector_status_job.outputs.url }} 50 | echo ${{ github.event.sha }} 51 | - name: Check the URL 52 | run: curl -L --fail ${{ needs.circleci_artifacts_redirector_status_job.outputs.url }} | grep ${{ github.event.sha }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # comment this out distribution branches 2 | node_modules/ 3 | 4 | # Editors 5 | .vscode 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Other Dependency directories 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | 65 | # next.js build output 66 | .next 67 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | line-length: disable 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 GitHub Actions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # circleci-artifacts-redirector-action 2 | 3 | GitHub Action to add a GitHub status link to a CircleCI artifact. 4 | 5 | ## Example usage 6 | 7 | Sample `.github/workflows/main.yml`: 8 | 9 | ```YAML 10 | on: [status] 11 | 12 | permissions: read-all 13 | 14 | jobs: 15 | circleci_artifacts_redirector_job: 16 | runs-on: ubuntu-latest 17 | if: "${{ github.event.context == 'ci/circleci: build_doc' }}" 18 | permissions: 19 | statuses: write 20 | name: Run CircleCI artifacts redirector 21 | steps: 22 | - name: GitHub Action step 23 | id: step1 24 | uses: scientific-python/circleci-artifacts-redirector-action@v1 # or use hash for better security 25 | with: 26 | repo-token: ${{ secrets.GITHUB_TOKEN }} 27 | api-token: ${{ secrets.CIRCLECI_TOKEN }} 28 | artifact-path: 0/test_artifacts/root_artifact.md 29 | circleci-jobs: build_doc 30 | job-title: Check the rendered docs here! 31 | - name: Check the URL 32 | if: github.event.status != 'pending' 33 | run: | 34 | curl --fail ${{ steps.step1.outputs.url }} | grep $GITHUB_SHA 35 | 36 | ``` 37 | - The `if: "${{ github.event.context == 'ci/circleci: build_doc' }}"` 38 | conditional in the `job` is helpful to limit the number of redirector 39 | actions that your repo will run to avoid polluting your GitHub actions 40 | logs. The `circleci-jobs` (below) should be labeled correspondingly. 41 | - The `api-token` needs to be a 42 | [CircleCI personal API token](https://app.circleci.com/settings/user/tokens) 43 | or a [CircleCI project API token](https://circleci.com/docs/managing-api-tokens/#creating-a-project-api-token) 44 | whose value has been added to the GitHub secrets of your repository (e.g., as 45 | `CIRCLECI_TOKEN`), e.g. for the MNE-Python *project* this would be 46 | https://github.com/mne-tools/mne-python/settings/secrets/actions and for the 47 | *organization* it would be https://github.com/organizations/mne-tools/settings/secrets/actions (pick whichever scope makes sense for you). 48 | - The `artifact-path` should point to an artifact path from your CircleCI 49 | build. This is typically whatever follows the CircleCI artifact root path, 50 | for example `0/test_artifacts/root_artifact.md`. 51 | - The `circleci-jobs` is a comma-separated list of jobs to monitor, but usually 52 | there is a single one that you want an artifact path for. 53 | The default is `"build_docs,build,doc"`, which will look for any 54 | jobs with these names and create an artifacts link for them. If you have 55 | multiple jobs to consider, make sure you adjust your `if:` statement (above) 56 | correspondingly. 57 | - The `job-title` corresponds to the name of the action job as it will appear 58 | on github. It is **not** the circle-ci job you want the artifacts for 59 | (this is `circleci-jobs`). This field is optional. 60 | - The action has an outtput ``url`` that you can use in downstream steps, but 61 | this URL will only point to a valid artifact once the job is complete, i.e., 62 | `github.event.status` is either `'success'`, `'fail'`, or (maybe) `'error'`, 63 | not `'pending'`. 64 | - If you have trouble, try [enabling debugging logging](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging) 65 | for the action by setting the secret `ACTIONS_STEP_DEBUG=true`. 66 | 67 | > **Note**: The standard PR-to-main-repo-from-branch-in-a-fork workflow might 68 | > not activate the action. For changes to take effect, changes might need to be 69 | > made to to the default branch *in a repo with the action enabled*. For 70 | > example, you could iterate directly in `master`, or in `master` of a fork. 71 | > This seems to be a limitation of the fact that CircleCI uses the `status` 72 | > (rather than app) API and that this is always tied to the `master`/default 73 | > branch of a given repository. 74 | 75 | ## Limitations 76 | 77 | Currently has (known) limitations: 78 | 79 | - Tests do not test anything (haven't gotten around to fixing them) 80 | - Only allows redirecting to a single file that must be configured ahead of 81 | time as a file (cannot be changed within the CircleCI run) 82 | 83 | Eventually this might be fixable by a bit of work and addition of 84 | customization options. 85 | 86 | ## Contributing 87 | 88 | Make any changes needed to `package-lock.json` and `index.js` and open a PR.These changes will automatically be compiled into `dist/index.js` by the 89 | [autofix.ci bot](https://autofix.ci/). 90 | 91 | If you want to do the same work locally as the bot, use `npm install` to get 92 | all dependencies and then call `ncc build index.js`. On Ubuntu you might 93 | need to `export NODE_OPTIONS=--openssl-legacy-provider` before the `ncc build` 94 | step. 95 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'run-circleci-artifacts-redirector' 2 | description: 'Create a direct artifact link upon CircleCI completion' 3 | branding: 4 | icon: check 5 | color: black 6 | author: 'Eric Larson' 7 | inputs: 8 | repo-token: 9 | description: 'The GITHUB_TOKEN secret' 10 | required: true 11 | artifact-path: 12 | description: 'Path to the CircleCI artifact' 13 | required: true 14 | circleci-jobs: 15 | description: 'Comma-separated list of CircleCI jobs to monitor (default is "build_docs,build,doc")' 16 | required: false 17 | api-token: 18 | description: 'CircleCI API token secret, only needed if the repository is private' 19 | required: false 20 | job-title: 21 | description: 'The name of the job, as it will render on the PR GUI (default is " artifact)"' 22 | required: false 23 | outputs: 24 | url: 25 | description: 'The full redirect URL' 26 | on: status 27 | runs: 28 | using: 'node20' 29 | main: 'dist/index.js' 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // This as annoying because CircleCI does not use the App API. 2 | // Hence we must monitor statuses rather than using the more convenient 3 | // "checks" API. 4 | // 5 | // After changing this file, use `ncc build index.js` to rebuild to dist/ 6 | 7 | // Refs: 8 | // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#status 9 | 10 | const core = require('@actions/core') 11 | const github = require('@actions/github') 12 | const fetch = require('node-fetch'); 13 | 14 | async function run() { 15 | try { 16 | core.debug((new Date()).toTimeString()) 17 | const payload = github.context.payload 18 | const path = core.getInput('artifact-path', {required: true}) 19 | const token = core.getInput('repo-token', {required: true}) 20 | var apiToken = core.getInput('api-token', {required: false}) 21 | var circleciJobs = core.getInput('circleci-jobs', {required: false}) 22 | if (circleciJobs === '') { 23 | circleciJobs = 'build_docs,doc,build' 24 | } 25 | const prepender = x => `ci/circleci: ${x}` 26 | circleciJobs = circleciJobs.split(',').map(prepender) 27 | core.debug(`Considering CircleCI jobs named: ${circleciJobs}`) 28 | if (circleciJobs.indexOf(payload.context) < 0) { 29 | core.debug(`Ignoring context: ${payload.context}`) 30 | return 31 | } 32 | const state = payload.state 33 | core.debug(`context: ${payload.context}`) 34 | core.debug(`state: ${state}`) 35 | core.debug(`target_url: ${payload.target_url}`) 36 | // e.g., https://circleci.com/gh/mne-tools/mne-python/53315 37 | // e.g., https://circleci.com/gh/scientific-python/circleci-artifacts-redirector-action/94?utm_campaign=vcs-integration-link&utm_medium=referral&utm_source=github-build-link 38 | // Set the new status 39 | const parts = payload.target_url.split('?')[0].split('/') 40 | const orgId = parts.slice(-3)[0] 41 | const repoId = parts.slice(-2)[0] 42 | const buildId = parts.slice(-1)[0] 43 | core.debug(`org: ${orgId}`) 44 | core.debug(`repo: ${repoId}`) 45 | core.debug(`build: ${buildId}`) 46 | // Get the URLs 47 | const artifacts_url = `https://circleci.com/api/v2/project/gh/${orgId}/${repoId}/${buildId}/artifacts` 48 | core.debug(`Fetching JSON: ${artifacts_url}`) 49 | if (apiToken == null || apiToken == '') { 50 | apiToken = 'null' 51 | } 52 | else { 53 | core.debug(`Successfully read CircleCI API token ${apiToken}`) 54 | } 55 | const headers = {'Circle-Token': apiToken, 'accept': 'application/json', 'user-agent': 'curl/7.85.0'} 56 | // e.g., https://circleci.com/api/v2/project/gh/scientific-python/circleci-artifacts-redirector-action/94/artifacts 57 | const response = await fetch(artifacts_url, {headers}) 58 | const artifacts = await response.json() 59 | core.debug(`Artifacts JSON (status=${response.status}):`) 60 | core.debug(artifacts) 61 | // e.g., {"next_page_token":null,"items":[{"path":"test_artifacts/root_artifact.md","node_index":0,"url":"https://output.circle-artifacts.com/output/job/6fdfd148-31da-4a30-8e89-a20595696ca5/artifacts/0/test_artifacts/root_artifact.md"}]} 62 | var url = ''; 63 | if (artifacts.items.length > 0) { 64 | url = `${artifacts.items[0].url.split('/artifacts/')[0]}/artifacts/${path}` 65 | } 66 | else { 67 | url = payload.target_url; 68 | } 69 | core.debug(`Linking to: ${url}`) 70 | core.debug((new Date()).toTimeString()) 71 | core.setOutput("url", url) 72 | const client = github.getOctokit(token) 73 | var description = ''; 74 | if (payload.state === 'pending') { 75 | description = 'Waiting for CircleCI ...' 76 | } 77 | else { 78 | description = `Link to ${path}` 79 | } 80 | var job_title = core.getInput('job-title', {required: false}) 81 | if (job_title === '') { 82 | job_title = `${payload.context} artifact` 83 | } 84 | return client.rest.repos.createCommitStatus({ 85 | repo: github.context.repo.repo, 86 | owner: github.context.repo.owner, 87 | sha: payload.sha, 88 | state: state, 89 | target_url: url, 90 | description: description, 91 | context: job_title 92 | }) 93 | } catch (error) { 94 | core.setFailed(error.message) 95 | } 96 | } 97 | 98 | run() 99 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | describe('TODO - Add a test suite', () => { 2 | it('TODO - Add a test', async () => { 3 | }); 4 | }); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript-action", 3 | "version": "1.0.0", 4 | "description": "JavaScript Action Template", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint index.js", 8 | "package": "ncc build index.js -o dist", 9 | "test": "eslint index.js && jest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/actions/javascript-action.git" 14 | }, 15 | "keywords": [ 16 | "GitHub", 17 | "Actions", 18 | "JavaScript" 19 | ], 20 | "author": "GitHub", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/actions/javascript-action/issues" 24 | }, 25 | "homepage": "https://github.com/actions/javascript-action#readme", 26 | "dependencies": { 27 | "@actions/core": "^1.10.0", 28 | "@actions/github": "^6.0.0", 29 | "node-fetch": "^2.7.0" 30 | }, 31 | "devDependencies": { 32 | "@vercel/ncc": "^0.38.3", 33 | "eslint": "^6.8.0", 34 | "jest": "^27.5.1" 35 | } 36 | } 37 | --------------------------------------------------------------------------------