forgejo/modules/actions/workflows_test.go
Andreas Ahlenstorf af1eda733c
Some checks are pending
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing-integration / test-sqlite (push) Waiting to run
testing-integration / test-mariadb (v10.6) (push) Waiting to run
testing-integration / test-mariadb (v11.8) (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
feat(actions): make GITHUB_WORKFLOW_REF available (#10276)
Make the variable `GITHUB_WORKFLOW_REF` available in Forgejo Action workflows. It is the ref path to the workflow and looks like `testowner/testrepo/.forgejo/workflows/test-workflow.yaml@refs/heads/main` ([GitHub documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/variables)). GitHub Actions like [gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) rely on its presence. See https://code.forgejo.org/forgejo/forgejo-actions-feature-requests/issues/56 for additional details.

`GITHUB_WORKFLOW_REF` cannot be generated easily during an action run. Either the path to workflow file has to be hardcoded or inferred by replicating the logic Forgejo uses to determine it. That is further complicated by the fact that Forgejo supports multiple search paths, namely `.forgejo/workflows`, `.gitea/workflows`, and `.github/workflows`. It is also the reason that the workflow directory is now stored in the database alongside the name of the workflow file.

Partial implementation is required in Forgejo Runner, see https://code.forgejo.org/forgejo/runner/pulls/1197.

Example workflow:

```yaml
on:
  push:
  workflow_dispatch:
  schedule:
    - cron: "* * * * *"
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "FORGEJO_WORKFLOW_REF=$FORGEJO_WORKFLOW_REF"
          echo "GITHUB_WORKFLOW_REF=$GITHUB_WORKFLOW_REF"
          echo "forgejo.workflow_ref=${{ forgejo.workflow_ref }}"
          echo "github.workflow_ref=${{ github.workflow_ref }}"
```

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [ ] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] I do not want this change to show in the release notes.
- [ ] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10276
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Co-authored-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
Co-committed-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
2025-12-17 23:15:26 +01:00

398 lines
14 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"os"
"path/filepath"
"testing"
"time"
"forgejo.org/modules/git"
"forgejo.org/modules/setting"
api "forgejo.org/modules/structs"
"forgejo.org/modules/test"
webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestActionsWorkflowsDetectMatched(t *testing.T) {
testCases := []struct {
desc string
commit *git.Commit
triggeredEvent webhook_module.HookEventType
payload api.Payloader
yamlOn string
expected bool
}{
{
desc: "HookEventCreate(create) matches GithubEventCreate(create)",
triggeredEvent: webhook_module.HookEventCreate,
payload: nil,
yamlOn: "on: create",
expected: true,
},
{
desc: "HookEventIssues(issues) `opened` action matches GithubEventIssues(issues)",
triggeredEvent: webhook_module.HookEventIssues,
payload: &api.IssuePayload{Action: api.HookIssueOpened},
yamlOn: "on: issues",
expected: true,
},
{
desc: "HookEventIssueComment(issue_comment) `created` action matches GithubEventIssueComment(issue_comment)",
triggeredEvent: webhook_module.HookEventIssueComment,
payload: &api.IssueCommentPayload{Action: api.HookIssueCommentCreated},
yamlOn: "on:\n issue_comment:\n types: [created]",
expected: true,
},
{
desc: "HookEventIssues(issues) `milestoned` action matches GithubEventIssues(issues)",
triggeredEvent: webhook_module.HookEventIssues,
payload: &api.IssuePayload{Action: api.HookIssueMilestoned},
yamlOn: "on: issues",
expected: true,
},
{
desc: "HookEventPullRequestSync(pull_request_sync) matches GithubEventPullRequest(pull_request)",
triggeredEvent: webhook_module.HookEventPullRequestSync,
payload: &api.PullRequestPayload{Action: api.HookIssueSynchronized},
yamlOn: "on: pull_request",
expected: true,
},
{
desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
triggeredEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
yamlOn: "on: pull_request",
expected: false,
},
{
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
triggeredEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueClosed},
yamlOn: "on: pull_request",
expected: false,
},
{
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with branches",
triggeredEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{
Action: api.HookIssueClosed,
PullRequest: &api.PullRequest{
Base: &api.PRBranchInfo{},
},
},
yamlOn: "on:\n pull_request:\n branches: [main]",
expected: false,
},
{
desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type",
triggeredEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
yamlOn: "on:\n pull_request:\n types: [labeled]",
expected: true,
},
{
desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)",
triggeredEvent: webhook_module.HookEventPullRequestReviewComment,
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
yamlOn: "on:\n pull_request_review_comment:\n types: [created]",
expected: true,
},
{
desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match GithubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)",
triggeredEvent: webhook_module.HookEventPullRequestReviewRejected,
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
yamlOn: "on:\n pull_request_review:\n types: [dismissed]",
expected: false,
},
{
desc: "HookEventRelease(release) `published` action matches GithubEventRelease(release) with `published` activity type",
triggeredEvent: webhook_module.HookEventRelease,
payload: &api.ReleasePayload{Action: api.HookReleasePublished},
yamlOn: "on:\n release:\n types: [published]",
expected: true,
},
{
desc: "HookEventRelease(updated) `updated` action matches GithubEventRelease(edited) with `edited` activity type",
triggeredEvent: webhook_module.HookEventRelease,
payload: &api.ReleasePayload{Action: api.HookReleaseUpdated},
yamlOn: "on:\n release:\n types: [edited]",
expected: true,
},
{
desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type",
triggeredEvent: webhook_module.HookEventPackage,
payload: &api.PackagePayload{Action: api.HookPackageCreated},
yamlOn: "on:\n registry_package:\n types: [updated]",
expected: false,
},
{
desc: "HookEventWiki(wiki) matches GithubEventGollum(gollum)",
triggeredEvent: webhook_module.HookEventWiki,
payload: nil,
yamlOn: "on: gollum",
expected: true,
},
{
desc: "HookEventSchedule(schedule) matches GithubEventSchedule(schedule)",
triggeredEvent: webhook_module.HookEventSchedule,
payload: nil,
yamlOn: "on: schedule",
expected: true,
},
{
desc: "HookEventWorkflowDispatch(workflow_dispatch) matches GithubEventWorkflowDispatch(workflow_dispatch)",
triggeredEvent: webhook_module.HookEventWorkflowDispatch,
payload: nil,
yamlOn: "on: workflow_dispatch",
expected: true,
},
{
desc: "push to tag matches workflow with paths condition (should skip paths check)",
triggeredEvent: webhook_module.HookEventPush,
payload: &api.PushPayload{
Ref: "refs/tags/v1.0.0",
Before: "0000000",
Commits: []*api.PayloadCommit{
{
ID: "abcdef123456",
Added: []string{"src/main.go"},
Message: "Release v1.0.0",
},
},
},
commit: nil,
yamlOn: "on:\n push:\n paths:\n - src/**",
expected: true,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
evts, err := GetEventsFromContent([]byte(tc.yamlOn))
require.NoError(t, err)
assert.Len(t, evts, 1)
assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggeredEvent, tc.payload, evts[0]))
})
}
}
func TestActionsWorkflowsListWorkflowsReturnsNoWorkflowsIfThereAreNone(t *testing.T) {
t.Cleanup(test.MockVariableValue(&setting.Git.HomePath, t.TempDir()))
require.NoError(t, git.InitSimple(t.Context()))
committer := git.Signature{
Email: "jane@example.com",
Name: "Jane",
When: time.Now(),
}
repoHome := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(repoHome, "README.md"), []byte("My project"), 0o644))
require.NoError(t, git.InitRepository(t.Context(), repoHome, false, git.Sha1ObjectFormat.Name()))
require.NoError(t, git.AddChanges(repoHome, true))
require.NoError(t, git.CommitChanges(repoHome, git.CommitChangesOptions{Message: "Import", Committer: &committer}))
gitRepo, err := git.OpenRepository(t.Context(), repoHome)
require.NoError(t, err)
defer gitRepo.Close()
headBranch, err := gitRepo.GetHEADBranch()
require.NoError(t, err)
lastCommitID, err := gitRepo.GetBranchCommitID(headBranch.Name)
require.NoError(t, err)
lastCommit, err := gitRepo.GetCommit(lastCommitID)
require.NoError(t, err)
source, workflows, err := ListWorkflows(lastCommit)
require.NoError(t, err)
assert.Empty(t, source)
assert.Empty(t, workflows)
}
func TestActionsWorkflowsListWorkflowsIgnoresNonWorkflowFiles(t *testing.T) {
t.Cleanup(test.MockVariableValue(&setting.Git.HomePath, t.TempDir()))
require.NoError(t, git.InitSimple(t.Context()))
committer := git.Signature{
Email: "jane@example.com",
Name: "Jane",
When: time.Now(),
}
githubWorkflow := []byte(`
name: GitHub Workflow
on:
push:
jobs:
do-something:
runs-on: ubuntu-latest
steps:
- run: echo 'Hello GitHub'
`)
repoHome := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(repoHome, ".forgejo/workflows"), os.ModePerm))
require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".forgejo/workflows", "README.md"), []byte("My project"), 0o644))
// Prepare a valid workflow in .github/workflows to verify that it is ignored because .forgejo/workflows is present.
require.NoError(t, os.MkdirAll(filepath.Join(repoHome, ".github/workflows"), os.ModePerm))
require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".github/workflows", "github.yaml"), githubWorkflow, 0o644))
require.NoError(t, git.InitRepository(t.Context(), repoHome, false, git.Sha1ObjectFormat.Name()))
require.NoError(t, git.AddChanges(repoHome, true))
require.NoError(t, git.CommitChanges(repoHome, git.CommitChangesOptions{Message: "Import", Committer: &committer}))
gitRepo, err := git.OpenRepository(t.Context(), repoHome)
require.NoError(t, err)
defer gitRepo.Close()
headBranch, err := gitRepo.GetHEADBranch()
require.NoError(t, err)
lastCommitID, err := gitRepo.GetBranchCommitID(headBranch.Name)
require.NoError(t, err)
lastCommit, err := gitRepo.GetCommit(lastCommitID)
require.NoError(t, err)
source, workflows, err := ListWorkflows(lastCommit)
require.NoError(t, err)
assert.Equal(t, ".forgejo/workflows", source)
assert.Empty(t, workflows)
}
func TestActionsWorkflowsListWorkflowsReturnsForgejoWorkflowsOnly(t *testing.T) {
t.Cleanup(test.MockVariableValue(&setting.Git.HomePath, t.TempDir()))
require.NoError(t, git.InitSimple(t.Context()))
committer := git.Signature{
Email: "jane@example.com",
Name: "Jane",
When: time.Now(),
}
forgejoWorkflow := []byte(`
name: Forgejo Workflow
on:
push:
jobs:
do-something:
runs-on: ubuntu-latest
steps:
- run: echo 'Hello Forgejo'
`)
githubWorkflow := []byte(`
name: GitHub Workflow
on:
push:
jobs:
do-something:
runs-on: ubuntu-latest
steps:
- run: echo 'Hello GitHub'
`)
repoHome := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(repoHome, ".forgejo/workflows"), os.ModePerm))
require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".forgejo/workflows", "forgejo.yaml"), forgejoWorkflow, 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(repoHome, ".github/workflows"), os.ModePerm))
require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".github/workflows", "github.yaml"), githubWorkflow, 0o644))
require.NoError(t, git.InitRepository(t.Context(), repoHome, false, git.Sha1ObjectFormat.Name()))
require.NoError(t, git.AddChanges(repoHome, true))
require.NoError(t, git.CommitChanges(repoHome, git.CommitChangesOptions{Message: "Import", Committer: &committer}))
gitRepo, err := git.OpenRepository(t.Context(), repoHome)
require.NoError(t, err)
defer gitRepo.Close()
headBranch, err := gitRepo.GetHEADBranch()
require.NoError(t, err)
lastCommitID, err := gitRepo.GetBranchCommitID(headBranch.Name)
require.NoError(t, err)
lastCommit, err := gitRepo.GetCommit(lastCommitID)
require.NoError(t, err)
source, workflows, err := ListWorkflows(lastCommit)
require.NoError(t, err)
assert.Len(t, workflows, 1)
assert.Equal(t, ".forgejo/workflows", source)
assert.Equal(t, "forgejo.yaml", workflows[0].Name())
}
func TestActionsWorkflowsListWorkflowsReturnsGitHubWorkflowsIfForgejoWorkflowsAbsent(t *testing.T) {
t.Cleanup(test.MockVariableValue(&setting.Git.HomePath, t.TempDir()))
require.NoError(t, git.InitSimple(t.Context()))
committer := git.Signature{
Email: "jane@example.com",
Name: "Jane",
When: time.Now(),
}
buildWorkflow := []byte(`
name: Build
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo 'We are building'
`)
testWorkflow := []byte(`
name: Test
on:
push:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo 'We are testing'
`)
repoHome := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(repoHome, ".github/workflows"), os.ModePerm))
require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".github/workflows", "build.yaml"), buildWorkflow, 0o644))
require.NoError(t, os.WriteFile(filepath.Join(repoHome, ".github/workflows", "test.yml"), testWorkflow, 0o644))
require.NoError(t, git.InitRepository(t.Context(), repoHome, false, git.Sha1ObjectFormat.Name()))
require.NoError(t, git.AddChanges(repoHome, true))
require.NoError(t, git.CommitChanges(repoHome, git.CommitChangesOptions{Message: "Import", Committer: &committer}))
gitRepo, err := git.OpenRepository(t.Context(), repoHome)
require.NoError(t, err)
defer gitRepo.Close()
headBranch, err := gitRepo.GetHEADBranch()
require.NoError(t, err)
lastCommitID, err := gitRepo.GetBranchCommitID(headBranch.Name)
require.NoError(t, err)
lastCommit, err := gitRepo.GetCommit(lastCommitID)
require.NoError(t, err)
source, workflows, err := ListWorkflows(lastCommit)
require.NoError(t, err)
assert.Len(t, workflows, 2)
assert.Equal(t, ".github/workflows", source)
assert.Equal(t, "build.yaml", workflows[0].Name())
assert.Equal(t, "test.yml", workflows[1].Name())
}