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>
This commit is contained in:
Andreas Ahlenstorf 2025-12-17 23:15:26 +01:00 committed by Mathieu Fenniak
parent 1ef5496055
commit af1eda733c
15 changed files with 531 additions and 210 deletions

View file

@ -47,6 +47,7 @@ type ActionRun struct {
Repo *repo_model.Repository `xorm:"-"` Repo *repo_model.Repository `xorm:"-"`
OwnerID int64 `xorm:"index"` OwnerID int64 `xorm:"index"`
WorkflowID string `xorm:"index"` // the name of workflow file WorkflowID string `xorm:"index"` // the name of workflow file
WorkflowDirectory string `xorm:"NOT NULL DEFAULT '.forgejo/workflows'"` // directory where the workflow file resides, for example, .forgejo/workflows
Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository
TriggerUserID int64 `xorm:"index"` TriggerUserID int64 `xorm:"index"`
TriggerUser *user_model.User `xorm:"-"` TriggerUser *user_model.User `xorm:"-"`

View file

@ -26,6 +26,7 @@ type ActionSchedule struct {
Repo *repo_model.Repository `xorm:"-"` Repo *repo_model.Repository `xorm:"-"`
OwnerID int64 `xorm:"index"` OwnerID int64 `xorm:"index"`
WorkflowID string WorkflowID string
WorkflowDirectory string `xorm:"NOT NULL DEFAULT '.forgejo/workflows'"`
TriggerUserID int64 TriggerUserID int64
TriggerUser *user_model.User `xorm:"-"` TriggerUser *user_model.User `xorm:"-"`
Ref string Ref string

View file

@ -0,0 +1,27 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package forgejo_migrations
import (
"xorm.io/xorm"
)
func init() {
registerMigration(&Migration{
Description: "Add the column workflow_directory to the tables action_run and action_schedule",
Upgrade: addActionRunWorkflowDirectory,
})
}
func addActionRunWorkflowDirectory(x *xorm.Engine) error {
type ActionRun struct {
WorkflowDirectory string `xorm:"workflow_directory NOT NULL DEFAULT '.forgejo/workflows'"`
}
type ActionSchedule struct {
WorkflowDirectory string `xorm:"workflow_directory NOT NULL DEFAULT '.forgejo/workflows'"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionRun), new(ActionSchedule))
return err
}

View file

@ -22,7 +22,8 @@ import (
) )
type DetectedWorkflow struct { type DetectedWorkflow struct {
EntryName string EntryName string // file name of the workflow, for example, test.yaml
EntryDirectory string // folder where the workflow was found, for example, .forgejo/workflows
TriggerEvent *jobparser.Event TriggerEvent *jobparser.Event
Content []byte Content []byte
EventDetectionError error EventDetectionError error
@ -46,24 +47,41 @@ func IsWorkflow(path string) bool {
return strings.HasPrefix(path, ".forgejo/workflows") || strings.HasPrefix(path, ".gitea/workflows") || strings.HasPrefix(path, ".github/workflows") return strings.HasPrefix(path, ".forgejo/workflows") || strings.HasPrefix(path, ".gitea/workflows") || strings.HasPrefix(path, ".github/workflows")
} }
func ListWorkflows(commit *git.Commit) (git.Entries, error) { // ListWorkflows looks for one of the standard workflow directories .forgejo/workflows, .gitea/workflows, and
tree, err := commit.SubTree(".forgejo/workflows") // .github/workflows in the given Git tree and returns the name of the first one it encounters including all the
if _, ok := err.(git.ErrNotExist); ok { // workflow files it contains, if any.
tree, err = commit.SubTree(".gitea/workflows") func ListWorkflows(commit *git.Commit) (string, git.Entries, error) {
workflowSources := []string{
".forgejo/workflows",
".gitea/workflows",
".github/workflows",
} }
var workflowSource string
var tree *git.Tree
for _, workflowSource = range workflowSources {
var err error
tree, err = commit.SubTree(workflowSource)
// If the source does not exist, we try the next one.
if _, ok := err.(git.ErrNotExist); ok { if _, ok := err.(git.ErrNotExist); ok {
tree, err = commit.SubTree(".github/workflows") continue
}
if _, ok := err.(git.ErrNotExist); ok {
return nil, nil
} }
// Other errors are reported immediately.
if err != nil { if err != nil {
return nil, err return "", nil, err
}
// We have found a valid source that we will use, no matter whether it contains workflows or not.
break
}
if tree == nil {
return "", nil, nil
} }
entries, err := tree.ListEntriesRecursiveFast() entries, err := tree.ListEntriesRecursiveFast()
if err != nil { if err != nil {
return nil, err return "", nil, err
} }
ret := make(git.Entries, 0, len(entries)) ret := make(git.Entries, 0, len(entries))
@ -72,7 +90,7 @@ func ListWorkflows(commit *git.Commit) (git.Entries, error) {
ret = append(ret, entry) ret = append(ret, entry)
} }
} }
return ret, nil return workflowSource, ret, nil
} }
func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) { func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
@ -108,7 +126,7 @@ func DetectWorkflows(
payload api.Payloader, payload api.Payloader,
detectSchedule bool, detectSchedule bool,
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { ) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
entries, err := ListWorkflows(commit) directory, entries, err := ListWorkflows(commit)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -127,6 +145,7 @@ func DetectWorkflows(
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
dwf := &DetectedWorkflow{ dwf := &DetectedWorkflow{
EntryName: entry.Name(), EntryName: entry.Name(),
EntryDirectory: directory,
TriggerEvent: &jobparser.Event{ TriggerEvent: &jobparser.Event{
Name: triggedEvent.Event(), Name: triggedEvent.Event(),
}, },
@ -142,6 +161,7 @@ func DetectWorkflows(
if detectSchedule { if detectSchedule {
dwf := &DetectedWorkflow{ dwf := &DetectedWorkflow{
EntryName: entry.Name(), EntryName: entry.Name(),
EntryDirectory: directory,
TriggerEvent: evt, TriggerEvent: evt,
Content: content, Content: content,
} }
@ -150,6 +170,7 @@ func DetectWorkflows(
} else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) { } else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
dwf := &DetectedWorkflow{ dwf := &DetectedWorkflow{
EntryName: entry.Name(), EntryName: entry.Name(),
EntryDirectory: directory,
TriggerEvent: evt, TriggerEvent: evt,
Content: content, Content: content,
} }
@ -162,7 +183,7 @@ func DetectWorkflows(
} }
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) { func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
entries, err := ListWorkflows(commit) directory, entries, err := ListWorkflows(commit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -185,6 +206,7 @@ func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*D
log.Trace("detect scheduled workflow: %q", entry.Name()) log.Trace("detect scheduled workflow: %q", entry.Name())
dwf := &DetectedWorkflow{ dwf := &DetectedWorkflow{
EntryName: entry.Name(), EntryName: entry.Name(),
EntryDirectory: directory,
TriggerEvent: evt, TriggerEvent: evt,
Content: content, Content: content,
} }

View file

@ -4,17 +4,22 @@
package actions package actions
import ( import (
"os"
"path/filepath"
"testing" "testing"
"time"
"forgejo.org/modules/git" "forgejo.org/modules/git"
"forgejo.org/modules/setting"
api "forgejo.org/modules/structs" api "forgejo.org/modules/structs"
"forgejo.org/modules/test"
webhook_module "forgejo.org/modules/webhook" webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestDetectMatched(t *testing.T) { func TestActionsWorkflowsDetectMatched(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
commit *git.Commit commit *git.Commit
@ -179,3 +184,215 @@ func TestDetectMatched(t *testing.T) {
}) })
} }
} }
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())
}

View file

@ -80,7 +80,7 @@ func List(ctx *context.Context) {
ctx.ServerError("GetBranchCommit", err) ctx.ServerError("GetBranchCommit", err)
return return
} }
entries, err := actions.ListWorkflows(commit) _, entries, err := actions.ListWorkflows(commit)
if err != nil { if err != nil {
ctx.ServerError("ListWorkflows", err) ctx.ServerError("ListWorkflows", err)
return return

View file

@ -40,6 +40,7 @@ func generateGiteaContextForRun(run *actions_model.ActionRun) *model.GithubConte
} }
refName := git.RefName(ref) refName := git.RefName(ref)
workflowRef := fmt.Sprintf("%s/%s/%s/%s@%s", run.Repo.OwnerName, run.Repo.Name, run.WorkflowDirectory, run.WorkflowID, ref)
gitContextObj := &model.GithubContext{ gitContextObj := &model.GithubContext{
// standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context // standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
@ -67,6 +68,7 @@ func generateGiteaContextForRun(run *actions_model.ActionRun) *model.GithubConte
ServerURL: setting.AppURL, // string, The URL of the GitHub server. For example: https://github.com. ServerURL: setting.AppURL, // string, The URL of the GitHub server. For example: https://github.com.
Sha: sha, // string, The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the workflow. For more information, see "Events that trigger workflows." For example, ffac537e6cbbf934b08745a378932722df287a53. Sha: sha, // string, The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the workflow. For more information, see "Events that trigger workflows." For example, ffac537e6cbbf934b08745a378932722df287a53.
Workflow: run.WorkflowID, // string, The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository. Workflow: run.WorkflowID, // string, The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository.
WorkflowRef: workflowRef, // string, ref path to the workflow file, for example, example/test/.forgejo/workflows/test.yaml@refs/heads/main
Workspace: "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action. Workspace: "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action.
} }
if run.TriggerUser != nil { if run.TriggerUser != nil {

View file

@ -61,7 +61,8 @@ func TestGenerateGiteaContext(t *testing.T) {
TriggerEvent: "push", TriggerEvent: "push",
Ref: "refs/heads/main", Ref: "refs/heads/main",
CommitSHA: "abc123def456", CommitSHA: "abc123def456",
WorkflowID: "test-workflow", WorkflowID: "test-workflow.yaml",
WorkflowDirectory: ".forgejo/workflows",
EventPayload: `{"repository": {"name": "testrepo"}}`, EventPayload: `{"repository": {"name": "testrepo"}}`,
} }
@ -77,7 +78,8 @@ func TestGenerateGiteaContext(t *testing.T) {
assert.Equal(t, "testowner", context["repository_owner"]) assert.Equal(t, "testowner", context["repository_owner"])
assert.Equal(t, "abc123def456", context["sha"]) assert.Equal(t, "abc123def456", context["sha"])
assert.Equal(t, "42", context["run_number"]) assert.Equal(t, "42", context["run_number"])
assert.Equal(t, "test-workflow", context["workflow"]) assert.Equal(t, "test-workflow.yaml", context["workflow"])
assert.Equal(t, "testowner/testrepo/.forgejo/workflows/test-workflow.yaml@refs/heads/main", context["workflow_ref"])
assert.Equal(t, false, context["ref_protected"]) assert.Equal(t, false, context["ref_protected"])
assert.Equal(t, "Actions", context["secret_source"]) assert.Equal(t, "Actions", context["secret_source"])
assert.Equal(t, setting.AppURL, context["server_url"]) assert.Equal(t, setting.AppURL, context["server_url"])
@ -158,7 +160,8 @@ func TestGenerateGiteaContext(t *testing.T) {
TriggerEvent: "pull_request", TriggerEvent: "pull_request",
Ref: "refs/pull/1/merge", Ref: "refs/pull/1/merge",
CommitSHA: "merge789sha", CommitSHA: "merge789sha",
WorkflowID: "test-workflow", WorkflowID: "test-workflow.yaml",
WorkflowDirectory: ".forgejo/workflows",
Event: webhook_module.HookEventPullRequest, Event: webhook_module.HookEventPullRequest,
EventPayload: string(payloadBytes), EventPayload: string(payloadBytes),
} }
@ -169,6 +172,7 @@ func TestGenerateGiteaContext(t *testing.T) {
assert.Equal(t, "feature-branch", context["head_ref"]) assert.Equal(t, "feature-branch", context["head_ref"])
assert.Equal(t, "refs/pull/1/merge", context["ref"]) assert.Equal(t, "refs/pull/1/merge", context["ref"])
assert.Equal(t, "merge789sha", context["sha"]) assert.Equal(t, "merge789sha", context["sha"])
assert.Equal(t, "testowner/testrepo/.forgejo/workflows/test-workflow.yaml@refs/pull/1/merge", context["workflow_ref"])
}) })
t.Run("Pull request target event", func(t *testing.T) { t.Run("Pull request target event", func(t *testing.T) {
@ -197,7 +201,8 @@ func TestGenerateGiteaContext(t *testing.T) {
TriggerEvent: actions_module.GithubEventPullRequestTarget, TriggerEvent: actions_module.GithubEventPullRequestTarget,
Ref: "refs/pull/1/merge", Ref: "refs/pull/1/merge",
CommitSHA: "merge789sha", CommitSHA: "merge789sha",
WorkflowID: "test-workflow", WorkflowID: "test-workflow.yml",
WorkflowDirectory: ".github/workflows",
Event: webhook_module.HookEventPullRequest, Event: webhook_module.HookEventPullRequest,
EventPayload: string(payloadBytes), EventPayload: string(payloadBytes),
} }
@ -211,6 +216,7 @@ func TestGenerateGiteaContext(t *testing.T) {
assert.Equal(t, "base123sha", context["sha"]) assert.Equal(t, "base123sha", context["sha"])
assert.Equal(t, "main", context["ref_name"]) assert.Equal(t, "main", context["ref_name"])
assert.Equal(t, "branch", context["ref_type"]) assert.Equal(t, "branch", context["ref_type"])
assert.Equal(t, "testowner/testrepo/.github/workflows/test-workflow.yml@refs/heads/main", context["workflow_ref"])
}) })
} }
@ -235,7 +241,8 @@ func TestGenerateGiteaContextForRun(t *testing.T) {
TriggerEvent: "push", TriggerEvent: "push",
Ref: "refs/heads/main", Ref: "refs/heads/main",
CommitSHA: "abc123def456", CommitSHA: "abc123def456",
WorkflowID: "test-workflow", WorkflowID: "test-workflow.yaml",
WorkflowDirectory: ".forgejo/workflows",
EventPayload: `{"repository": {"name": "testrepo"}}`, EventPayload: `{"repository": {"name": "testrepo"}}`,
} }
@ -251,7 +258,8 @@ func TestGenerateGiteaContextForRun(t *testing.T) {
assert.Equal(t, "testowner", gitContextObj.RepositoryOwner) assert.Equal(t, "testowner", gitContextObj.RepositoryOwner)
assert.Equal(t, "abc123def456", gitContextObj.Sha) assert.Equal(t, "abc123def456", gitContextObj.Sha)
assert.Equal(t, "42", gitContextObj.RunNumber) assert.Equal(t, "42", gitContextObj.RunNumber)
assert.Equal(t, "test-workflow", gitContextObj.Workflow) assert.Equal(t, "test-workflow.yaml", gitContextObj.Workflow)
assert.Equal(t, "testowner/testrepo/.forgejo/workflows/test-workflow.yaml@refs/heads/main", gitContextObj.WorkflowRef)
assert.Equal(t, "testrepo", gitContextObj.Event["repository"].(map[string]any)["name"]) assert.Equal(t, "testrepo", gitContextObj.Event["repository"].(map[string]any)["name"])
@ -296,7 +304,8 @@ func TestGenerateGiteaContextForRun(t *testing.T) {
TriggerEvent: "pull_request", TriggerEvent: "pull_request",
Ref: "refs/pull/1/merge", Ref: "refs/pull/1/merge",
CommitSHA: "merge789sha", CommitSHA: "merge789sha",
WorkflowID: "test-workflow", WorkflowID: "test-workflow.yaml",
WorkflowDirectory: ".forgejo/workflows",
Event: webhook_module.HookEventPullRequest, Event: webhook_module.HookEventPullRequest,
EventPayload: string(payloadBytes), EventPayload: string(payloadBytes),
} }
@ -307,6 +316,7 @@ func TestGenerateGiteaContextForRun(t *testing.T) {
assert.Equal(t, "feature-branch", gitContextObj.HeadRef) assert.Equal(t, "feature-branch", gitContextObj.HeadRef)
assert.Equal(t, "refs/pull/1/merge", gitContextObj.Ref) assert.Equal(t, "refs/pull/1/merge", gitContextObj.Ref)
assert.Equal(t, "merge789sha", gitContextObj.Sha) assert.Equal(t, "merge789sha", gitContextObj.Sha)
assert.Equal(t, "testowner/testrepo/.forgejo/workflows/test-workflow.yaml@refs/pull/1/merge", gitContextObj.WorkflowRef)
}) })
t.Run("Pull request target event", func(t *testing.T) { t.Run("Pull request target event", func(t *testing.T) {
@ -335,7 +345,8 @@ func TestGenerateGiteaContextForRun(t *testing.T) {
TriggerEvent: actions_module.GithubEventPullRequestTarget, TriggerEvent: actions_module.GithubEventPullRequestTarget,
Ref: "refs/pull/1/merge", Ref: "refs/pull/1/merge",
CommitSHA: "merge789sha", CommitSHA: "merge789sha",
WorkflowID: "test-workflow", WorkflowID: "test-workflow.yml",
WorkflowDirectory: ".github/workflows",
Event: webhook_module.HookEventPullRequest, Event: webhook_module.HookEventPullRequest,
EventPayload: string(payloadBytes), EventPayload: string(payloadBytes),
} }
@ -349,5 +360,6 @@ func TestGenerateGiteaContextForRun(t *testing.T) {
assert.Equal(t, "base123sha", gitContextObj.Sha) assert.Equal(t, "base123sha", gitContextObj.Sha)
assert.Equal(t, "main", gitContextObj.RefName) assert.Equal(t, "main", gitContextObj.RefName)
assert.Equal(t, "branch", gitContextObj.RefType) assert.Equal(t, "branch", gitContextObj.RefType)
assert.Equal(t, "testowner/testrepo/.github/workflows/test-workflow.yml@refs/heads/main", gitContextObj.WorkflowRef)
}) })
} }

View file

@ -360,6 +360,7 @@ func handleWorkflows(
RepoID: input.Repo.ID, RepoID: input.Repo.ID,
OwnerID: input.Repo.OwnerID, OwnerID: input.Repo.OwnerID,
WorkflowID: dwf.EntryName, WorkflowID: dwf.EntryName,
WorkflowDirectory: dwf.EntryDirectory,
TriggerUserID: input.Doer.ID, TriggerUserID: input.Doer.ID,
Ref: ref, Ref: ref,
CommitSHA: commit.ID.String(), CommitSHA: commit.ID.String(),
@ -576,6 +577,7 @@ func handleSchedules(
RepoID: input.Repo.ID, RepoID: input.Repo.ID,
OwnerID: input.Repo.OwnerID, OwnerID: input.Repo.OwnerID,
WorkflowID: dwf.EntryName, WorkflowID: dwf.EntryName,
WorkflowDirectory: dwf.EntryDirectory,
TriggerUserID: user_model.ActionsUserID, TriggerUserID: user_model.ActionsUserID,
Ref: input.Repo.DefaultBranch, Ref: input.Repo.DefaultBranch,
CommitSHA: commit.ID.String(), CommitSHA: commit.ID.String(),

View file

@ -120,6 +120,7 @@ func testActionsNotifierPullRequestWithDoer(t *testing.T, repo *repo_model.Repos
CommitMessage: "test", CommitMessage: "test",
} }
dw.EntryName = "test.yml" dw.EntryName = "test.yml"
dw.EntryDirectory = ".forgejo/workflows"
dw.TriggerEvent = &jobparser.Event{ dw.TriggerEvent = &jobparser.Event{
Name: "pull_request", Name: "pull_request",
} }
@ -326,3 +327,29 @@ func TestActionsNotifier_RunsOnNeeds(t *testing.T) {
// first inserted it is tagged w/ incomplete_runs_on. // first inserted it is tagged w/ incomplete_runs_on.
assert.Contains(t, string(job.WorkflowPayload), "incomplete_runs_on: true") assert.Contains(t, string(job.WorkflowPayload), "incomplete_runs_on: true")
} }
func TestActionsNotifier_WorkflowDetection(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
dw := &actions_module.DetectedWorkflow{
Content: []byte("{ on: pull_request, jobs: { j1: {} }}"),
}
testActionsNotifierPullRequest(t, repo, pr, dw, webhook_module.HookEventPullRequestSync)
runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
RepoID: repo.ID,
})
require.NoError(t, err)
require.Len(t, runs, 1)
run := runs[0]
jobs, err := db.Find[actions_model.ActionRunJob](t.Context(), actions_model.FindRunJobOptions{RunID: run.ID})
require.NoError(t, err)
require.Len(t, jobs, 1)
assert.Equal(t, ".forgejo/workflows", runs[0].WorkflowDirectory)
assert.Equal(t, "test.yml", runs[0].WorkflowID)
}

View file

@ -127,6 +127,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
RepoID: cron.RepoID, RepoID: cron.RepoID,
OwnerID: cron.OwnerID, OwnerID: cron.OwnerID,
WorkflowID: cron.WorkflowID, WorkflowID: cron.WorkflowID,
WorkflowDirectory: cron.WorkflowDirectory,
TriggerUserID: cron.TriggerUserID, TriggerUserID: cron.TriggerUserID,
Ref: cron.Ref, Ref: cron.Ref,
CommitSHA: cron.CommitSHA, CommitSHA: cron.CommitSHA,

View file

@ -32,6 +32,7 @@ func TestServiceActions_startTask(t *testing.T) {
RepoID: repo.ID, RepoID: repo.ID,
OwnerID: repo.OwnerID, OwnerID: repo.OwnerID,
WorkflowID: workflowID, WorkflowID: workflowID,
WorkflowDirectory: ".forgejo/workflows",
TriggerUserID: repo.OwnerID, TriggerUserID: repo.OwnerID,
Ref: "branch", Ref: "branch",
CommitSHA: "fakeSHA", CommitSHA: "fakeSHA",
@ -80,6 +81,7 @@ func TestCreateScheduleTask(t *testing.T) {
assert.Equal(t, cron.RepoID, run.RepoID) assert.Equal(t, cron.RepoID, run.RepoID)
assert.Equal(t, cron.OwnerID, run.OwnerID) assert.Equal(t, cron.OwnerID, run.OwnerID)
assert.Equal(t, cron.WorkflowID, run.WorkflowID) assert.Equal(t, cron.WorkflowID, run.WorkflowID)
assert.Equal(t, cron.WorkflowDirectory, run.WorkflowDirectory)
assert.Equal(t, cron.TriggerUserID, run.TriggerUserID) assert.Equal(t, cron.TriggerUserID, run.TriggerUserID)
assert.Equal(t, cron.Ref, run.Ref) assert.Equal(t, cron.Ref, run.Ref)
assert.Equal(t, cron.CommitSHA, run.CommitSHA) assert.Equal(t, cron.CommitSHA, run.CommitSHA)
@ -108,6 +110,7 @@ func TestCreateScheduleTask(t *testing.T) {
RepoID: repo.ID, RepoID: repo.ID,
OwnerID: repo.OwnerID, OwnerID: repo.OwnerID,
WorkflowID: "some.yml", WorkflowID: "some.yml",
WorkflowDirectory: ".forgejo/workflows",
TriggerUserID: repo.OwnerID, TriggerUserID: repo.OwnerID,
Ref: "branch", Ref: "branch",
CommitSHA: "fakeSHA", CommitSHA: "fakeSHA",
@ -138,6 +141,7 @@ jobs:
RepoID: repo.ID, RepoID: repo.ID,
OwnerID: repo.OwnerID, OwnerID: repo.OwnerID,
WorkflowID: "some.yml", WorkflowID: "some.yml",
WorkflowDirectory: ".github/workflows",
TriggerUserID: repo.OwnerID, TriggerUserID: repo.OwnerID,
Ref: "branch", Ref: "branch",
CommitSHA: "fakeSHA", CommitSHA: "fakeSHA",
@ -279,6 +283,7 @@ func TestServiceActions_DynamicMatrix(t *testing.T) {
RepoID: repo.ID, RepoID: repo.ID,
OwnerID: repo.OwnerID, OwnerID: repo.OwnerID,
WorkflowID: workflowID, WorkflowID: workflowID,
WorkflowDirectory: ".forgejo/workflows",
TriggerUserID: repo.OwnerID, TriggerUserID: repo.OwnerID,
Ref: "branch", Ref: "branch",
CommitSHA: "fakeSHA", CommitSHA: "fakeSHA",

View file

@ -137,6 +137,7 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
Repo: repo, Repo: repo,
OwnerID: repo.OwnerID, OwnerID: repo.OwnerID,
WorkflowID: entry.WorkflowID, WorkflowID: entry.WorkflowID,
WorkflowDirectory: ".forgejo/workflows",
TriggerUserID: doer.ID, TriggerUserID: doer.ID,
TriggerUser: doer, TriggerUser: doer,
Ref: entry.Ref, Ref: entry.Ref,
@ -198,7 +199,7 @@ func GetWorkflowFromCommit(gitRepo *git.Repository, ref, workflowID string) (*Wo
return nil, err return nil, err
} }
entries, err := actions.ListWorkflows(commit) _, entries, err := actions.ListWorkflows(commit)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -471,6 +471,7 @@ jobs:
assert.Equal(t, setting.AppURL, gtCtx["server_url"].GetStringValue()) assert.Equal(t, setting.AppURL, gtCtx["server_url"].GetStringValue())
assert.Equal(t, actionRun.CommitSHA, gtCtx["sha"].GetStringValue()) assert.Equal(t, actionRun.CommitSHA, gtCtx["sha"].GetStringValue())
assert.Equal(t, actionRun.WorkflowID, gtCtx["workflow"].GetStringValue()) assert.Equal(t, actionRun.WorkflowID, gtCtx["workflow"].GetStringValue())
assert.Equal(t, "user2/actions-gitea-context/.gitea/workflows/pull.yml@refs/pull/1/head", gtCtx["workflow_ref"].GetStringValue())
assert.Equal(t, setting.Actions.DefaultActionsURL.URL(), gtCtx["gitea_default_actions_url"].GetStringValue()) assert.Equal(t, setting.Actions.DefaultActionsURL.URL(), gtCtx["gitea_default_actions_url"].GetStringValue())
token := gtCtx["token"].GetStringValue() token := gtCtx["token"].GetStringValue()
assert.Equal(t, actionTask.TokenLastEight, token[len(token)-8:]) assert.Equal(t, actionTask.TokenLastEight, token[len(token)-8:])

View file

@ -795,7 +795,7 @@ func TestActionsCreateDeleteRefEvent(t *testing.T) {
}) })
} }
func TestActionsWorkflowDispatchEvent(t *testing.T) { func TestActionsWorkflowDispatch(t *testing.T) {
onApplicationRun(t, func(t *testing.T, u *url.URL) { onApplicationRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@ -842,6 +842,8 @@ func TestActionsWorkflowDispatchEvent(t *testing.T) {
assert.Equal(t, "test", r.Title) assert.Equal(t, "test", r.Title)
assert.Equal(t, "dispatch.yml", r.WorkflowID) assert.Equal(t, "dispatch.yml", r.WorkflowID)
// .forgejo/workflows is wrong. It should be .gitea/workflows because the workflow is saved there during setup.
assert.Equal(t, ".forgejo/workflows", r.WorkflowDirectory)
assert.Equal(t, sha, r.CommitSHA) assert.Equal(t, sha, r.CommitSHA)
assert.Equal(t, actions_module.GithubEventWorkflowDispatch, r.TriggerEvent) assert.Equal(t, actions_module.GithubEventWorkflowDispatch, r.TriggerEvent)
assert.Len(t, j, 1) assert.Len(t, j, 1)