Skip to content

Commit 09aeef7

Browse files
authored
feat: Add custom GitHub Actions to a separate repository (#1)
I was adding the usage of our custom GitHub Actions to a lot of repositories, and it felt confusing that actions live in a repository called `workflows`. I've moved them to this new repository which will contain just actions, so that you can use them as it would make sense, e.g.: ```yaml - name: Commit to a new branch uses: apify/actions/signed-commit@v1.0.0 with: commit-message: "chore: add greeting" ``` I've also done some changes to simplify the setup: - I've rewritten the `execute-workflow` action to use a composite action using `github-script`, so that it doesn't need to be compiled (since `github-script` can run TypeScript files natively) - I've renamed the `commit` action to `signed-commit` to make it clearer why it's used just from the name After this is merged, I will replace all the relevant usages of `apify/workflows` in other repositories with `apify/actions` and then delete the custom actions from the `workflows` repository.
1 parent bb5e721 commit 09aeef7

32 files changed

Lines changed: 3708 additions & 0 deletions

.editorconfig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 4
6+
charset = utf-8
7+
trim_trailing_whitespace = true
8+
insert_final_newline = true
9+
end_of_line = lf
10+
# editorconfig-tools is unable to ignore longs strings or urls
11+
max_line_length = null
12+
13+
[{*.yaml,*.yml,*.json}]
14+
indent_size = 2

.github/workflows/checks.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Lint and test actions
2+
3+
on:
4+
workflow_dispatch:
5+
6+
push:
7+
branches:
8+
- '**'
9+
tags-ignore:
10+
- '**'
11+
12+
jobs:
13+
test_actions:
14+
runs-on: ubuntu-22.04-arm64
15+
steps:
16+
- name: Checkout repo
17+
uses: actions/checkout@v6
18+
19+
- name: Setup Node.js
20+
uses: actions/setup-node@v6
21+
with:
22+
node-version-file: '.nvmrc'
23+
check-latest: true
24+
25+
- name: Setup pnpm
26+
uses: pnpm/action-setup@v6
27+
28+
- name: Install dependencies
29+
run: pnpm install --frozen-lockfile --ignore-scripts
30+
31+
- name: Lint
32+
run: pnpm run lint
33+
34+
- name: Typecheck
35+
run: pnpm run type-check
36+
37+
- name: Test
38+
run: pnpm run test
39+

.github/workflows/release.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# This workflow creates new github release and updates changelog
2+
name: release new version
3+
4+
permissions:
5+
contents: write
6+
pull-requests: write
7+
8+
on:
9+
workflow_dispatch:
10+
11+
push:
12+
branches:
13+
- main
14+
15+
jobs:
16+
release-please:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: release-please
20+
uses: googleapis/release-please-action@v4
21+
with:
22+
release-type: simple

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
.idea
3+
.vscode
4+
.venv
5+
.mise.toml
6+
.mypy_cache
7+
node_modules/
8+
*.tsbuildinfo

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
24

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Apify GitHub Actions
2+
3+
This repository contains reusable GitHub Actions used in internal Apify projects.
4+
5+
## How to release new version
6+
7+
1. Create a PR. **IMPORTANT: Avoid using the `chore:` prefix, as it doesn't work with RELEASE-PLEASE. Use `feat:` or `fix:` instead.**
8+
2. Merge PR into the main branch after approval. This triggers an automated workflow that generates a new PR for the release using the RELEASE-PLEASE action.
9+
3. Navigate to the PR and merge it into the main branch. This will publish the release with an updated changelog.

execute-workflow/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Execute Workflow Action
2+
3+
A GitHub Action for triggering workflows via `workflow_dispatch`, waiting for them to complete, and reporting their results.
4+
5+
- Triggers workflows using the `workflow_dispatch` event
6+
- Waits for the workflow run to start (up to 60 seconds)
7+
- Polls the workflow run status until completion
8+
- Outputs the workflow run ID and conclusion
9+
- Fails the action if the triggered workflow doesn't succeed
10+
11+
## Usage
12+
13+
```yaml
14+
- name: Execute workflow
15+
uses: ./execute-workflow
16+
with:
17+
workflow: my-workflow.yaml
18+
inputs: '{ "key": "value" }'
19+
```
20+
21+
## Inputs
22+
23+
- `workflow` (required): Workflow filename relative to `.github/workflows` (e.g., `my-workflow.yaml`)
24+
- `inputs` (optional): Inputs to pass to the workflow as a JSON string
25+
- `token` (optional): GitHub token for authentication (defaults to `${{ github.token }}`)
26+
27+
## Outputs
28+
29+
- `workflowRunId`: The ID of the triggered workflow run
30+
- `conclusion`: The conclusion of the workflow run (`success`, `failure`, `cancelled`, etc.)

execute-workflow/action.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: 'Execute Workflow'
2+
description: 'Execute GitHub Actions workflows and wait for them to complete'
3+
4+
inputs:
5+
workflow:
6+
description: "Workflow filename relative to .github/workflows (e.g., 'my-workflow.yaml')"
7+
required: true
8+
inputs:
9+
description: 'Inputs to pass to the workflow, must be a JSON string'
10+
required: false
11+
token:
12+
description: 'GitHub token for authentication'
13+
required: false
14+
default: ${{ github.token }}
15+
16+
runs:
17+
using: composite
18+
steps:
19+
- name: Execute workflow
20+
uses: actions/github-script@v8
21+
with:
22+
github-token: ${{ inputs.token }}
23+
script: |
24+
const { main } = require('${{ github.action_path }}/index.mts');
25+
26+
await main({
27+
github,
28+
context,
29+
core,
30+
});

execute-workflow/index.mts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import type * as Core from '@actions/core';
2+
import { type Octokit } from '@octokit/rest';
3+
import { type context as Context } from '@actions/github';
4+
5+
import * as PackageJSON from '../package.json' with { type: 'json' };
6+
7+
type Workflow = {
8+
id: number;
9+
name: string;
10+
path: string;
11+
};
12+
13+
type WorkflowRun = {
14+
id: number;
15+
status: string;
16+
conclusion: string | null;
17+
html_url: string;
18+
};
19+
20+
export async function main({ github, context, core }: { github: Octokit, context: typeof Context, core: typeof Core }) {
21+
core.info(`🏃 Execute Workflow Action v${PackageJSON.version}`);
22+
try {
23+
const workflowFileName = core.getInput('workflow');
24+
25+
const inputsJson = core.getInput('inputs');
26+
const inputs = inputsJson ? JSON.parse(inputsJson) : {};
27+
28+
const { owner, repo } = context.repo;
29+
const { ref } = context;
30+
31+
const workflows: Workflow[] = await github.paginate(
32+
github.rest.actions.listRepoWorkflows.endpoint.merge({
33+
owner,
34+
repo,
35+
}),
36+
);
37+
38+
const workflowPath = `.github/workflows/${workflowFileName}`;
39+
const foundWorkflow = workflows.find((workflow) => workflow.path === workflowPath);
40+
41+
if (!foundWorkflow) throw new Error(`Unable to find workflow '${workflowPath}' in ${owner}/${repo} 😥`);
42+
43+
core.info(
44+
`🔎 Found workflow, id: ${foundWorkflow.id}, name: ${foundWorkflow.name}, path: ${foundWorkflow.path}`,
45+
);
46+
47+
// Get current workflow runs before dispatching
48+
const runsBefore = await github.rest.actions.listWorkflowRuns({
49+
owner,
50+
repo,
51+
workflow_id: foundWorkflow.id,
52+
per_page: 5,
53+
});
54+
55+
// Call workflow_dispatch API
56+
core.info('🚀 Calling GitHub API to dispatch workflow...');
57+
await github.request(`POST /repos/${owner}/${repo}/actions/workflows/${foundWorkflow.id}/dispatches`, {
58+
ref,
59+
inputs,
60+
});
61+
62+
// Wait for the new run to appear
63+
core.info('⏳ Waiting for workflow run to start...');
64+
let workflowRun: WorkflowRun | undefined;
65+
const maxWaitTime = 60000; // 60 seconds
66+
const pollInterval = 1000; // 1 second
67+
const startTime = Date.now();
68+
69+
while (!workflowRun && Date.now() - startTime < maxWaitTime) {
70+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
71+
72+
const runsAfter = await github.rest.actions.listWorkflowRuns({
73+
owner,
74+
repo,
75+
workflow_id: foundWorkflow.id,
76+
per_page: 5,
77+
});
78+
79+
// Find the new run (one that wasn't in the before list)
80+
const newRun = runsAfter.data.workflow_runs.find(
81+
(run) => !runsBefore.data.workflow_runs.some((oldRun) => oldRun.id === run.id),
82+
);
83+
84+
if (newRun) {
85+
workflowRun = newRun as WorkflowRun;
86+
}
87+
}
88+
89+
if (!workflowRun) {
90+
throw new Error('Timeout waiting for workflow run to start');
91+
}
92+
93+
core.info(`✅ Workflow run started: ${workflowRun.html_url}`);
94+
core.setOutput('workflowRunId', workflowRun.id);
95+
96+
// Poll until the workflow completes
97+
core.info('⏳ Waiting for workflow run to complete...');
98+
while (workflowRun.status !== 'completed') {
99+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
100+
101+
const runStatus = await github.rest.actions.getWorkflowRun({
102+
owner,
103+
repo,
104+
run_id: workflowRun.id,
105+
});
106+
107+
workflowRun = runStatus.data as WorkflowRun;
108+
core.info(`📊 Status: ${workflowRun.status}`);
109+
}
110+
111+
core.info(`🏁 Workflow run completed with conclusion: ${workflowRun.conclusion}`);
112+
core.setOutput('conclusion', workflowRun.conclusion);
113+
114+
if (workflowRun.conclusion !== 'success') {
115+
throw new Error(`Workflow run failed with conclusion: ${workflowRun.conclusion}`);
116+
}
117+
} catch (error) {
118+
const e = error as Error;
119+
120+
if (e.message.endsWith('a disabled workflow')) {
121+
core.warning('Workflow is disabled, no action was taken');
122+
return;
123+
}
124+
125+
core.setFailed(e.message);
126+
}
127+
}

0 commit comments

Comments
 (0)