diff --git a/.github/workflows/check-dist.yml b/.github/workflows/check-dist.yml index 5798200..6f8a4c3 100644 --- a/.github/workflows/check-dist.yml +++ b/.github/workflows/check-dist.yml @@ -7,7 +7,7 @@ # If this workflow is run from a feature branch, it will act as an additional CI # check and fail if the checked-in `dist/` directory does not match what is # expected from the build. -name: Check Transpiled JavaScript +name: Check on: push: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90218a1..8217dbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Continuous Integration +name: CI on: pull_request: @@ -51,13 +51,52 @@ jobs: id: checkout uses: actions/checkout@v4 - - name: Test Local Action - id: test-action + - name: Test Local Action (Minimal) + id: test-action-minimal uses: ./ with: - axiom_datasets: ${{ secrets.AXIOM_DATASETS }} - axiom_token: ${{ secrets.AXIOM_TOKEN }} - axiom_org_id: ${{ secrets.AXIOM_ORG_ID }} - title: ${{github.event.ref}} - description: ${{ github.event.head_commit.message }} - url: 'https://example.com' + token: ${{ secrets.AXIOM_TOKEN }} + axiomApiUrl: ${{ secrets.AXIOM_API_URL }} + datasets: annotations-action-ci + type: "minimal-example" + - name: Test Local Action (Complete) + id: test-action-complete + uses: ./ + with: + token: ${{ secrets.AXIOM_TOKEN }} + axiomApiUrl: ${{ secrets.AXIOM_API_URL }} + datasets: annotations-action-ci + time: "2024-01-01T00:00:00Z" + endTime: "2024-01-01T01:00:00Z" + title: "Complete example" + description: "This annotation has all the fields!" + url: "https://axiom.co" + type: "complete-example" + - name: "Install Hurl" + run: | + VERSION=4.3.0 + curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VERSION/hurl_${VERSION}_amd64.deb + sudo apt update && sudo apt install ./hurl_${VERSION}_amd64.deb + - run: | + hurl < [!IMPORTANT] -> -> Make sure to remove or update the [`CODEOWNERS`](./CODEOWNERS) file! For -> details on how to use this file, see -> [About code owners](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners). - -## Initial Setup - -After you've cloned the repository to your local machine or codespace, you'll -need to perform some initial setup steps before you can develop your action. - -> [!NOTE] -> -> You'll need to have a reasonably modern version of -> [Node.js](https://nodejs.org) handy (20.x or later should work!). If you are -> using a version manager like [`nodenv`](https://github.com/nodenv/nodenv) or -> [`nvm`](https://github.com/nvm-sh/nvm), this template has a `.node-version` -> file at the root of the repository that will be used to automatically switch -> to the correct version when you `cd` into the repository. Additionally, this -> `.node-version` file is used by GitHub Actions in any `actions/setup-node` -> actions. - -1. :hammer_and_wrench: Install the dependencies - - ```bash - npm install - ``` - -1. :building_construction: Package the TypeScript for distribution - - ```bash - npm run bundle - ``` - -1. :white_check_mark: Run the tests - - ```bash - $ npm test - - PASS ./index.test.js - ✓ throws invalid number (3ms) - ✓ wait 500 ms (504ms) - ✓ test runs (95ms) - - ... - ``` - -## Update the Action Metadata - -The [`action.yml`](action.yml) file defines metadata about your action, such as -input(s) and output(s). For details about this file, see -[Metadata syntax for GitHub Actions](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions). - -When you copy this repository, update `action.yml` with the name, description, -inputs, and outputs for your action. - -## Update the Action Code - -The [`src/`](./src/) directory is the heart of your action! This contains the -source code that will be run when your action is invoked. You can replace the -contents of this directory with your own code. - -There are a few things to keep in mind when writing your action code: - -- Most GitHub Actions toolkit and CI/CD operations are processed asynchronously. - In `main.ts`, you will see that the action is run in an `async` function. - - ```javascript - import * as core from '@actions/core' - //... - - async function run() { - try { - //... - } catch (error) { - core.setFailed(error.message) - } - } - ``` - - For more information about the GitHub Actions toolkit, see the - [documentation](https://github.com/actions/toolkit/blob/master/README.md). - -So, what are you waiting for? Go ahead and start customizing your action! - -1. Create a new branch - - ```bash - git checkout -b releases/v1 - ``` - -1. Replace the contents of `src/` with your action code -1. Add tests to `__tests__/` for your source code -1. Format, test, and build the action - - ```bash - npm run all - ``` - - > [!WARNING] - > - > This step is important! It will run [`ncc`](https://github.com/vercel/ncc) - > to build the final JavaScript action code with all dependencies included. - > If you do not run this step, your action will not work correctly when it is - > used in a workflow. This step also includes the `--license` option for - > `ncc`, which will create a license file for all of the production node - > modules used in your project. - -1. Commit your changes - - ```bash - git add . - git commit -m "My first action is ready!" - ``` - -1. Push them to your repository - - ```bash - git push -u origin releases/v1 - ``` - -1. Create a pull request and get feedback on your action -1. Merge the pull request into the `main` branch - -Your action is now published! :rocket: - -For information about versioning your action, see -[Versioning](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md) -in the GitHub Actions toolkit. - -## Validate the Action - -You can now validate the action by referencing it in a workflow file. For -example, [`ci.yml`](./.github/workflows/ci.yml) demonstrates how to reference an -action in the same repository. - -```yaml -steps: - - name: Checkout - id: checkout - uses: actions/checkout@v4 - - - name: Test Local Action - id: test-action - uses: ./ - with: - milliseconds: 1000 - - - name: Print Output - id: output - run: echo "${{ steps.test-action.outputs.time }}" -``` - -For example workflow runs, check out the -[Actions tab](https://github.com/actions/typescript-action/actions)! :rocket: +This action allows you to create an annotation in Axiom. ## Usage -After testing, you can create version tag(s) that developers can use to -reference different stable versions of your action. For more information, see -[Versioning](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md) -in the GitHub Actions toolkit. - -To include the action in a workflow in another repository, you can use the -`uses` syntax with the `@` symbol to reference a specific branch, tag, or commit -hash. +1. Create an Axiom API token with permission to create annotations +2. Create your annotation: ```yaml -steps: - - name: Checkout - id: checkout - uses: actions/checkout@v4 - - - name: Test Local Action - id: test-action - uses: actions/typescript-action@v1 # Commit with the `v1` tag - with: - milliseconds: 1000 - - - name: Print Output - id: output - run: echo "${{ steps.test-action.outputs.time }}" +- name: Create annotation + uses: axiomhq/annotation-action@v1 + with: + token: ${{ secrets.AXIOM_TOKEN }} + datasets: production-logs + type: "production-release" + title: "Production release" # optional + description: "Commit ${{ github.sha }}" # optional + time: "2024-01-01T00:00:00Z" # optional, defaults to now + endTime: "2024-01-01T01:00:00Z" # optional, defaults to null + url: "https://axiom.co" # optional, defaults to the current job url ``` -## Publishing a New Release - -This project includes a helper script, [`script/release`](./script/release) -designed to streamline the process of tagging and pushing new releases for -GitHub Actions. - -GitHub Actions allows users to select a specific version of the action to use, -based on release tags. This script simplifies this process by performing the -following steps: - -1. **Retrieving the latest release tag:** The script starts by fetching the most - recent release tag by looking at the local data available in your repository. -1. **Prompting for a new release tag:** The user is then prompted to enter a new - release tag. To assist with this, the script displays the latest release tag - and provides a regular expression to validate the format of the new tag. -1. **Tagging the new release:** Once a valid new tag is entered, the script tags - the new release. -1. **Pushing the new tag to the remote:** Finally, the script pushes the new tag - to the remote repository. From here, you will need to create a new release in - GitHub and users can easily reference the new tag in their workflows. +This action has the ID of the annotation as an output, read the +[ci.yaml](https://github.com/axiomhq/annotation-action/blob/main/.github/workflows/ci.yml) +to see how to use it. diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts deleted file mode 100644 index f505353..0000000 --- a/__tests__/main.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Unit tests for the action's main functionality, src/main.ts - * - * These should be run as if the action was called from a workflow. - * Specifically, the inputs listed in `action.yml` should be set as environment - * variables following the pattern `INPUT_`. - */ - -import * as core from '@actions/core' -import * as main from '../src/main' - -// Mock the action's main function -const runMock = jest.spyOn(main, 'run') - -// Mock the GitHub Actions core library -let debugMock: jest.SpyInstance -let errorMock: jest.SpyInstance -let getInputMock: jest.SpyInstance -let setFailedMock: jest.SpyInstance - -describe('action', () => { - beforeEach(() => { - jest.clearAllMocks() - - debugMock = jest.spyOn(core, 'debug').mockImplementation() - errorMock = jest.spyOn(core, 'error').mockImplementation() - getInputMock = jest.spyOn(core, 'getInput').mockImplementation() - setFailedMock = jest.spyOn(core, 'setFailed').mockImplementation() - }) - - it('sends an annotation', async () => { - global.fetch = jest.fn(async () => - Promise.resolve({ - ok: true - }) - ) as unknown as typeof fetch - - // Set the action's inputs as return values from core.getInput() - getInputMock.mockImplementation((name: string) => { - switch (name) { - case 'axiom_datasets': - return 'test-dataset' - case 'axiom_token': - return 'test' - case 'title': - return 'test title' - case 'description': - return 'This is a test' - } - }) - - await main.run() - expect(runMock).toHaveReturned() - - // Verify that all of the core library functions were called correctly - expect(debugMock).toHaveBeenNthCalledWith(1, 'Sending annotation to Axiom') - expect(debugMock).toHaveBeenNthCalledWith( - 2, - `Annotation: ${JSON.stringify({ - datasets: ['test-dataset'], - title: 'test title', - description: 'This is a test', - url: `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` - })}` - ) - expect(setFailedMock).not.toHaveBeenCalled() - expect(debugMock).toHaveBeenNthCalledWith(3, 'Annotation sent to Axiom') - expect(errorMock).not.toHaveBeenCalled() - }) - - it('exits with failure if annotation failed to create', async () => { - global.fetch = jest.fn(async () => - Promise.resolve({ - ok: false, - status: 401, - statusText: 'Unauthorized' - }) - ) as unknown as typeof fetch - - // Set the action's inputs as return values from core.getInput() - getInputMock.mockImplementation((name: string) => { - switch (name) { - case 'axiom_datasets': - return 'test-dataset' - case 'axiom_token': - return 'test' - case 'title': - return 'test title' - case 'description': - return 'This is a test' - case 'url': - return 'https://example.com' - } - }) - - await main.run() - expect(runMock).toHaveReturned() - - // Verify that all of the core library functions were called correctly - expect(debugMock).toHaveBeenNthCalledWith(1, 'Sending annotation to Axiom') - expect(setFailedMock).toHaveBeenCalledWith( - 'Failed to send annotation to Axiom: Unauthorized' - ) - }) - - it('sets a failed status on error', async () => { - // Set the action's inputs as return values from core.getInput() - getInputMock.mockImplementation( - (name: string, options: { required: boolean }) => { - if (options && options.required) { - throw new Error(`Input required and not supplied: ${name}`) - } - } - ) - - await main.run() - expect(runMock).toHaveReturned() - - // Verify that all of the core library functions were called correctly - expect(setFailedMock).toHaveBeenNthCalledWith( - 1, - 'Input required and not supplied: axiom_datasets' - ) - expect(errorMock).not.toHaveBeenCalled() - }) -}) diff --git a/__tests__/wait.test.ts b/__tests__/wait.test.ts deleted file mode 100644 index 1336aaa..0000000 --- a/__tests__/wait.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Unit tests for src/wait.ts - */ - -import { wait } from '../src/wait' -import { expect } from '@jest/globals' - -describe('wait.ts', () => { - it('throws an invalid number', async () => { - const input = parseInt('foo', 10) - expect(isNaN(input)).toBe(true) - - await expect(wait(input)).rejects.toThrow('milliseconds not a number') - }) - - it('waits with a valid number', async () => { - const start = new Date() - await wait(500) - const end = new Date() - - const delta = Math.abs(end.getTime() - start.getTime()) - - expect(delta).toBeGreaterThan(450) - }) -}) diff --git a/action.yml b/action.yml index 1094d10..df48d06 100644 --- a/action.yml +++ b/action.yml @@ -1,32 +1,49 @@ -name: 'Axiom annotations' +name: 'Axiom annotation' +author: 'Axiom, Inc.' description: 'Create annotations in Axiom' -author: 'axiomhq' -# Add your action's branding here. This will appear on the GitHub Marketplace. branding: - icon: 'alert-triangle' + icon: 'message-square' color: 'black' -# Define your inputs here. inputs: - axiom_datasets: - description: 'Comma separated list of datasets to associate the annotations with' + axiomApiUrl: + description: 'The Axiom API URL, defaults to https://api.axiom.co' + required: false + default: '' + datasets: + description: 'Comma separated list of datasets to associate the annotation with' required: true - axiom_token: + token: description: 'Axiom API token' required: true - axiom_org_id: - description: 'Axiom organization ID' - required: true + time: + description: 'The time of the annotation (defaults to now)' + required: false + default: '' + endTime: + description: 'The end time of the annotation (if set, will create a ranged annotation)' + required: false + default: '' title: description: 'The title of the annotation' - required: true + required: false + default: '' description: - description: 'The description of the annotation (markdown supported)' - required: true + description: 'The description of the annotation (supports Markdown)' + required: false + default: '' url: - description: 'The URL to link to (defaults to the GitHub action URL)' + description: 'The URL to link to (defaults to the GitHub Actions run URL)' required: false + default: '' + type: + description: 'The type of the annotation, (e.g. production-deploy)' + required: true + +outputs: + id: + description: 'The ID of the created integration' runs: using: node20 diff --git a/dist/index.js b/dist/index.js index b928d53..fbc0095 100644 --- a/dist/index.js +++ b/dist/index.js @@ -24742,50 +24742,42 @@ const core = __importStar(__nccwpck_require__(2186)); */ async function run() { try { - const datasets = core - .getInput('axiom_datasets', { required: true }) - .split(','); - const token = core.getInput('axiom_token', { required: true }); - const orgId = core.getInput('axiom_org_id', { required: true }); - const title = core.getInput('title', { required: true }); - const description = core.getInput('description', { required: true }); - const url = core.getInput('url', { required: false }) ?? - `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; - // Debug logs are only output if the `ACTIONS_STEP_DEBUG` secret is true - core.debug('Sending annotation to Axiom'); - const body = { - type: 'deploy', - time: new Date().toISOString(), - datasets, - title, - description, - url + const axiomApiUrl = core.getInput('axiomApiURL') || 'https://api.axiom.co'; + const token = core.getInput('token', { required: true }); + core.setSecret(token); // Mask token in output + const payload = { + datasets: core.getInput('datasets', { required: true }).split(','), + time: core.getInput('time') || new Date().toISOString(), + endTime: core.getInput('endTime') || undefined, + title: core.getInput('title') || undefined, + description: core.getInput('description') || undefined, + url: core.getInput('url') || + `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`, + type: core.getInput('type', { required: true }) }; - // Log the annotation details - core.debug(`Annotation: ${JSON.stringify({ - datasets, - title, - description, - url - })}`); - // Send the annotation to Axiom - const response = await fetch('https://api.axiom.co/v2/annotations', { + core.debug(`Sending annotation to Axiom: ${JSON.stringify(payload)}`); + const response = await fetch(`${axiomApiUrl}/v2/annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - 'X-Axiom-Org-Id': orgId + Authorization: `Bearer ${token}` }, - body: JSON.stringify(body) + body: JSON.stringify(payload) }); - // Throw an error if the request fails if (!response.ok) { - throw new Error(`Failed to send annotation to Axiom: ${response.statusText}`); + let traceIdMsg = ''; + const traceId = response.headers.get('x-axiom-trace-id'); + if (traceId) { + traceIdMsg = ` (Trace ID: ${traceId})`; + } + throw new Error(`Failed to send annotation to Axiom${traceIdMsg}: ${response.statusText}`); } - core.debug('Annotation sent to Axiom'); + core.debug('Decoding response'); + const annotation = await response.json(); + core.setOutput('id', annotation.id); + core.info(`Created annotation ${annotation.id}`); } catch (error) { - // Fail the workflow run if an error occurs if (error instanceof Error) core.setFailed(error.message); } diff --git a/src/main.ts b/src/main.ts index 59b8370..efc5e11 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,61 +6,50 @@ import * as core from '@actions/core' */ export async function run(): Promise { try { - const datasets = core - .getInput('axiom_datasets', { required: true }) - .split(',') - const token = core.getInput('axiom_token', { required: true }) - const orgId = core.getInput('axiom_org_id', { required: true }) - const title = core.getInput('title', { required: true }) - const description = core.getInput('description', { required: true }) - const url = - core.getInput('url', { required: false }) ?? - `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` - - // Debug logs are only output if the `ACTIONS_STEP_DEBUG` secret is true - core.debug('Sending annotation to Axiom') - - const body = { - type: 'deploy', - time: new Date().toISOString(), - - datasets, - title, - description, - url + const axiomApiUrl = core.getInput('axiomApiURL') || 'https://api.axiom.co' + + const token = core.getInput('token', { required: true }) + core.setSecret(token) // Mask token in output + + const payload = { + datasets: core.getInput('datasets', { required: true }).split(','), + time: core.getInput('time') || new Date().toISOString(), + endTime: core.getInput('endTime') || undefined, + title: core.getInput('title') || undefined, + description: core.getInput('description') || undefined, + url: + core.getInput('url') || + `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`, + type: core.getInput('type', { required: true }) } - // Log the annotation details - core.debug( - `Annotation: ${JSON.stringify({ - datasets, - title, - description, - url - })}` - ) - - // Send the annotation to Axiom - const response = await fetch('https://api.axiom.co/v2/annotations', { + core.debug(`Sending annotation to Axiom: ${JSON.stringify(payload)}`) + const response = await fetch(`${axiomApiUrl}/v2/annotations`, { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - 'X-Axiom-Org-Id': orgId + Authorization: `Bearer ${token}` }, - body: JSON.stringify(body) + body: JSON.stringify(payload) }) - - // Throw an error if the request fails if (!response.ok) { + let traceIdMsg = '' + const traceId = response.headers.get('x-axiom-trace-id') + if (traceId) { + traceIdMsg = ` (Trace ID: ${traceId})` + } + throw new Error( - `Failed to send annotation to Axiom: ${response.statusText}` + `Failed to send annotation to Axiom${traceIdMsg}: ${response.statusText}` ) } - core.debug('Annotation sent to Axiom') + core.debug('Decoding response') + const annotation = await response.json() + + core.setOutput('id', annotation.id) + core.info(`Created annotation ${annotation.id}`) } catch (error) { - // Fail the workflow run if an error occurs if (error instanceof Error) core.setFailed(error.message) } }