The silent page awaits the waking dawn.
- name: Checkout repository
uses: https://data.forgejo.org/actions/checkout@v6
- name: Set up pnpm
uses: https://github.com/pnpm/action-setup@v4
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v4- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v4
- name: Log in to CodeFloe Container Registry
uses: https://github.com/docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.CODEFLOE_REGISTRY_USERNAME }}
password: ${{ secrets.CODEFLOE_REGISTRY_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: https://github.com/docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha
- name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v7
with:
context: .
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}docker login codefloe.com -u <组织成员用户名>
# 在密码提示中输入具有 read:package 权限的 PAT。
docker pull codefloe.com/your-private-org/yohaku:latest
Introduction
This post came about entirely by accident. GitHub forcibly changed my billing region to China, leaving my Free plan unable to renew and workflows in my private repositories completely unusable. (Yes, those are the workflows I wrote about in previous posts for syncing the Yohaku upstream and building Docker images.) Since I need to run workflows in private repositories, I reluctantly had to leave GitHub for another Git instance that supports CI for private projects. (So, GitHub, fxxk you.avif)
As luck would have it, I recently found a Forgejo instance with servers in Europe: CodeFloe. It has a more relaxed attitude toward AI than Codeberg, and the team recently set up a relay server in Singapore, which has made access much better. It also offers a full suite of services, including CI/CD (Actions + CrowCI), Weblate, Renovate, Pages, and MCP, so it was a natural choice for the move.
After moving the repositories from GitHub to Forgejo, the existing GitHub Actions workflows needed some adjustments to run on Forgejo Actions. So I grabbed an AI model (6.1 Sol) to handle the migration for me. What follows is its work.
This migration targets Forgejo Actions hosted on CodeFloe and covers three workflows: syncing code from a GitHub upstream, building and publishing Docker images, and deploying to EdgeOne Makers. After the migration, local syntax checks and key script tests were completed; this article does not treat those results as proof of a successful end-to-end run on a CodeFloe runner.
The configurations below can serve as a reference for migrating other projects. The runner labels and base images are specific to CodeFloe and will need to be adjusted for a self-hosted Forgejo instance. This article is based on the workflows used during the migration and official documentation consulted through November 8, 2026.
First, determine where workflows are loaded from
By default, Forgejo reads workflows from .forgejo/workflows/ in the repository. Only if that directory does not exist does it fall back to .github/workflows/.
You can therefore move the files you want to migrate to:
There is an easily overlooked consequence: once you create .forgejo/workflows/, Forgejo stops reading the remaining workflows in .github/workflows/.
If you originally had ten GitHub workflows and migrated only three, the other seven can remain available to GitHub, but they will not run automatically on Forgejo. Before migrating, decide which workflows need to run on the new platform.
You also need to enable Actions in the target repository settings and make sure the instance has an available runner. For scheduled jobs, put the workflow on the default branch; for manual runs, first check how the target instance discovers workflows.
Specify the Action source explicitly
A common pattern in GitHub Actions is:
This relies on the platform's configured default Action source. A Forgejo instance may use a different default from GitHub, and you should not assume CodeFloe defaults to GitHub either.
During migration, you can eliminate ambiguity by using a full URL:
But a full URL only answers where to download the Action. Being able to download an Action does not mean it will work properly on Forgejo.
You still need to check whether it calls the GitHub API, hardcodes github.com, relies on GitHub's cache or artifact services, and what Node.js and system environment it needs.
Recheck the runner instead of just keeping ubuntu-latest
GitHub-hosted runners usually come with a wide range of tools preinstalled. Forgejo runner environments are determined by the instance administrator, so even runners labeled ubuntu-latest may have different environments.
For tasks such as syncing code and running a Node.js CLI, this migration uses a container to pin the base environment:
This image provides the Node.js, git, and bash versions the project needs. Other projects may still need to provide Python, build tools, or additional system libraries themselves.
Docker builds need separate handling. Having the Docker CLI in a container does not mean it can connect to the Docker daemon. CodeFloe provides a docker runner label for this purpose:
The configuration explicitly specifies amd64 because the project's existing Dockerfile installs a dependency pinned to x64. Using only a generic label could assign the job to a runner with a different architecture.
For CodeFloe's Alpine Docker environment, this migration installs dependencies before running JavaScript Actions:
These commands are intended for Alpine. If a self-hosted runner uses Debian, switch to an appropriate image or package manager. CodeFloe's arm64 Docker runner has additional limitations; for example, overriding the container: image can make the Docker daemon unavailable. Follow the platform documentation when configuring it.
Distinguish repository, upstream, and registry tokens
Authentication needed the most rethinking in this migration. The GITHUB_TOKEN that worked on GitHub cannot simply take on every role on the new platform.
Forgejo creates a temporary token for workflows, exposing it through forgejo.token, FORGEJO_TOKEN, and other interfaces, while retaining compatibility names such as github.token and GITHUB_TOKEN.
This automatic token can be used to operate on the current repository, but there is one important behavior:
Different tasks therefore call for different credentials:
Operation
Credentials used in this migration
Pull the current Forgejo repository
Automatic job token
Push the branch adapted for deployment and continue deploying in subsequent steps
Automatic job token
Push `main` after syncing and trigger builds and deployments
CodeFloe PAT with repository write access
Pull the private GitHub upstream
GitHub PAT with upstream read access
Publish a CodeFloe container package
CodeFloe PAT with package write access
For example, the sync workflow needs to check out using a PAT so that subsequent pushes can trigger downstream workflows:
Secrets configured on GitHub do not automatically migrate with the Git repository; you must configure them again on CodeFloe.
Also, GitHub's:
is not proof that authorization will succeed on Forgejo. Forgejo handles some permission fields differently from GitHub, so determine actual access based on the automatic token, PAT scopes, organization permissions, and branch protection rules. Workflows using OIDC also need to be adjusted separately; Forgejo uses the enable-openid-connect setting.
Migrate both the build environment and the publishing target for Docker workflows
The original workflow publishes images to GHCR, typically authenticating with a GitHub user and GITHUB_TOKEN. If the destination changes to the CodeFloe container registry, the image address and login credentials need to change too.
You can explicitly declare the image path in the workflow:
The image path should be lowercase. If you need to publish to an organization, you also cannot simply keep using ${{ github.repository }}, because the code repository owner and the image's organization may not be the same.
You can retain Docker's official Actions for the build steps:
This explicitly initializes Buildx and disables build record uploads and summaries that depend on GitHub-specific services. If the original configuration uses type=gha caching, check whether the target instance supports it before deciding whether to switch to a registry cache or another solution.
The YAML above is a workflow fragment and must be combined with the runner, dependency setup, and checkout steps described earlier. You also need to retain any project-specific build arguments.
Image privacy depends on the owner
When migrating container registries, you also need to check access permissions. Forgejo packages belong to users or organizations, not code repositories.
An image built from a private code repository therefore does not automatically become private. According to the current Forgejo documentation, packages under public organizations are publicly readable, while packages under private organizations can be read only by organization members.
If you want to keep the code where it is while controlling image privacy independently, you can create a dedicated private organization and publish to:
The publishing account needs package write access in that organization, and the PAT must allow access to private resources rather than being limited to public resources. On the deployment server, first log in with an organization member account that has read access, then pull the image:
Publishing the new image to a private organization does not automatically change packages already published at the old address. You must handle those old packages and deployment references separately.
Inspect the Git operations in sync workflows
The original sync process used a third-party Action to pull from the GitHub upstream, then another Action to perform a rebase. Actions like these may hardcode the target platform, repository address, or authentication method.
When a workflow consists primarily of Git operations, using Git commands directly often makes it easier to inspect its behavior after migration. The retained process is:
The rewrite specifically addresses several issues:
The last safeguard uses explicit leases and an atomic push:
This requires recording the remote branches' commit IDs before the operation. If the remote supports atomic pushes, both branches can be updated together; if a branch changes during the run, the push will refuse to overwrite it.
Rebasing changes commit history, so whether it is appropriate depends on the project's existing collaboration practices. Also confirm that branch protection rules allow the corresponding updates.
Deployment workflows must check the actual command result
The EdgeOne deployment retains the original operational steps: merge main into the branch adapted for deployment, update submodules, push the branch, then build and deploy.
The migration focuses on pinning the runtime environment, using the right credentials, and reliably determining the deployment result. For example:
pipefail prevents a failed deployment command's exit status from being masked by a successful tee.
With JSON output, parse the structured result and check status and the access URL rather than relying on string searches to determine success. The URL returned by EdgeOne may contain required query parameters, so preserve it in full in logs and job summaries.
For concurrency, check whether the target Forgejo version supports it. This migration configures separate concurrency groups for syncing, image publishing, and deployment, with cancel-in-progress: false to prevent new triggers from arbitrarily interrupting an ongoing publishing operation.
Finally, verify behavior, not just YAML
After migration, verification can be done in three layers.
The first layer is static checks: whether the YAML parses, the fields are correct, the shell scripts have syntax errors, and Action URLs are explicit. This can catch configuration errors, but it cannot prove that authentication and network access work.
The second layer is script behavior testing. During this migration, tests in a temporary Git repository covered normal syncing, repeated runs, test mode, merge conflicts, rebase conflicts, and protection against concurrent updates. Deployment script tests covered successful results, error results, nonzero CLI exits, and invalid JSON.
The third layer is an actual run on the target platform. First use workflow_dispatch to verify the runner and credentials, then check actual image publishing, private image pulls, and whether pushing main triggers the full chain. The migration documented here has completed the first two layers; the third still requires confirmation from an actual run on the target platform.
When troubleshooting, use the symptoms to narrow down the cause:
Symptom
Check first
Workflow does not appear
Whether Actions is enabled, the directory location, and files on the default branch
Job stays queued
Whether runner labels and architecture match
Action download fails
Full URL, version reference, and network access
Node.js or bash reported missing
The job container's runtime environment
Cannot connect to Docker daemon
Whether a platform-supported Docker runner is in use
Registry login succeeds but push fails
PAT scopes, permissions in the organization that owns the image, and namespace
Sync push does not trigger deployment
Whether the push used the automatic job token
Image associated with a private repository can still be pulled anonymously
Visibility of the user or organization that owns the image
The lesson from this migration is to keep reusable YAML and build steps while checking each platform capability they depend on. Only after verifying directories, runners, authentication, the image registry, and downstream triggers can automation that ran on GitHub behave as expected on Forgejo.
References