The capacity to programmatically modify the file system within a GitHub Actions runner represents a fundamental pillar of Continuous Integration and Continuous Deployment (CI/CD) pipelines. Whether an organization is attempting to automate the generation of documentation, update versioning strings, or synchronize configuration files across environments, the ability to write data to a file is essential. This process involves a sophisticated interplay between the GitHub runner environment—typically an Ubuntu-based virtual machine—and specialized actions designed to abstract the complexities of file system I/O operations. By leveraging these tools, developers can transform a static repository into a dynamic environment where files are created, appended, or overwritten based on real-time event triggers, such as a code push or a manual workflow dispatch.
Orchestrating File Creation with DamianReeves/write-file-action
A primary mechanism for achieving file persistence within a workflow is the DamianReeves/write-file-action@master. This third-party action provides a streamlined interface for writing content to the runner's disk without requiring the author to write manual shell scripts for every basic file operation. It is important to note that this action is not certified by GitHub and is governed by its own separate terms of service and privacy policies, meaning users must evaluate its security profile before deploying it in production environments.
The action operates based on a set of strict input parameters that define how the file is handled on the disk.
| Parameter | Requirement | Description |
|---|---|---|
| path | Required | The absolute or relative path to the file that needs to be written. |
| contents | Required | The actual string data or multi-line block to be inserted into the file. |
| write-mode | Optional | Determines the behavior if the file already exists (overwrite, append, or preserve). |
The write-mode parameter is critical for determining the outcome of the operation. If the mode is set to overwrite, the action will completely replace the existing file if it already exists, ensuring a clean slate for the new content. When set to append, the action will attach the new content to the end of the existing file, which is ideal for logging or accumulating build artifacts. If the mode is set to preserve, the action will check for the existence of the file; if the file is already present, the action will skip the write operation entirely, preventing the accidental erasure of pre-existing data. The default behavior of the action, in the absence of a specified mode, is to append.
Upon successful execution, the action returns the size of the resulting file, providing a programmatic way to verify that data was actually written.
To implement this in a real-world scenario, such as modifying a .bashrc file in the home directory or creating a JavaScript file, the YAML configuration would look as follows:
```yaml
name: Overwrite some file
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Overwrite file
uses: "DamianReeves/write-file-action@master"
with:
path: path/to/file.js
write-mode: overwrite
contents: |
console.log('some contents')
- name: Commit & Push
uses: Andro999b/[email protected]
with:
githubtoken: ${{ secrets.GITHUBTOKEN }}
branch: main
force: true
message: 'Overwritten by Github Actions - ${date}'
```
In this configuration, the actions/checkout@v3 step is prerequisite because the runner must first have the repository context to know where the path/to/file.js should be placed relative to the workspace. Furthermore, the use of Andro999b/[email protected] demonstrates the necessary secondary step: because GitHub Actions runners are ephemeral, any file written using write-file-action only exists on that specific virtual machine. To make those changes permanent in the GitHub repository, a commit and push operation must be executed using a valid GITHUB_TOKEN.
Advanced Scripting with actions/github-script
While simple file writes are handled by dedicated actions, complex logic requiring interaction with the GitHub API and the workflow context is best handled by actions/github-script. This tool allows developers to write JavaScript directly within their YAML workflow, providing a bridge between the file system and the GitHub platform's metadata.
This action is particularly powerful when combined with external JavaScript modules. To use a script located within the repository, one must first use actions/checkout to make the file available on the runner. The implementation involves passing the github, context, and core objects to an external function.
Example implementation for an inline script:
yaml
on: push
jobs:
echo-input:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/github-script@v9
env:
SHA: '${{env.parentSHA}}'
with:
script: |
const script = require('./path/to/script.js')
await script({github, context, core})
The corresponding JavaScript module must export an asynchronous function to handle the API calls:
javascript
module.exports = async ({github, context, core}) => {
const {SHA} = process.env
const commit = await github.rest.repos.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `${SHA}`
})
core.exportVariable('author', commit.data.commit.author.email)
}
For users requiring external libraries, such as execa, the workflow must first initialize the Node.js environment. This is achieved by using actions/setup-node@v4 and running npm ci or npm install. This allows the github-script action to leverage a wrapper around require to import third-party modules, enabling the execution of complex shell commands or API interactions that would be impossible with a basic "write-to-file" action.
Fundamental Workflow Configuration and Setup
For any file-writing operation to occur, a valid GitHub Actions environment must be established. This requires a repository on GitHub with the Actions tab enabled. If the Actions tab is missing, it is likely disabled in the repository settings.
The structural requirement for GitHub to discover a workflow is strict: the workflow file must reside in the .github/workflows directory. This directory can be created in two ways:
- If the
.github/workflowsdirectory already exists, the user navigates to it and creates a new file. - If the directory does not exist, the user creates a file with the path
.github/workflows/filename.yml.
The file extension must be either .yml or .yaml, as YAML is the standard markup language for configuration files. A basic demonstration workflow, such as github-actions-demo.yml, utilizes various contexts to provide feedback on the runner's state:
```yaml
name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v6
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
```
This workflow illustrates that the github.workspace is the primary location where files are manipulated. Any action that writes a file, such as the DamianReeves/write-file-action, interacts with this workspace. Once the file is written, the user must commit the changes. In the "Propose changes" dialog, the user can commit directly to the default branch or create a new branch via a pull request.
Repository-Level File Interaction Challenges
A common technical challenge raised by developers is the ability to run language-specific scripts, such as PHP, to read, delete, and create files directly within a repository. For instance, a scenario where a PHP script reads JSON files and generates HTML pages for GitHub Pages.
While a PHP script can be executed within a GitHub Action on an ubuntu-latest runner, the script operates on a clone of the repository. The logic follows this sequence:
- The workflow triggers on a push to specific JSON files.
- The runner checks out the repository.
- The PHP script is executed, deleting old HTML files in a
docsfolder and creating new ones. - The resulting changes are local to the runner's disk.
The critical realization for developers is that no script (PHP, Python, or Node.js) can "directly" modify the remote GitHub repository files without a git commit and push cycle. To achieve the goal of automatically updating the repository with new HTML files, the workflow must include a step to push the changes back to the branch. This typically involves configuring a git user and using a token for authentication, as seen in the Andro999b/[email protected] example.
Analysis of File System Persistence and Runner Lifecycle
The interaction between writing a file and the persistence of that file is the most frequent point of failure for beginners. Because GitHub Actions employs ephemeral runners, any file written via write-file-action or a custom script is destroyed the moment the job completes.
The lifecycle of a file write operation consists of four distinct phases:
- Provisioning: The runner is initialized, and the
.github/workflowsfile is parsed. - Workspace Acquisition: The
actions/checkoutaction clones the repository into${{ github.workspace }}. - Manipulation: The
write-file-actionorgithub-scriptmodifies the disk. - Persistence: A push action sends the changes back to the GitHub server.
Without the persistence phase, the "write to file" operation is only useful for temporary build artifacts or configuration files needed for subsequent steps in the same job. If the intent is to update the repository's source code, the commit and push steps are mandatory.