The automation of software delivery pipelines relies heavily on the ability to execute specific logic not only during the primary build and deploy phases but also after those phases have concluded. In the context of GitHub Actions, "post" logic refers to the execution of commands, scripts, or webhooks that trigger following a specific event or the conclusion of a job. This capability is critical for maintaining system hygiene, notifying external services of deployment success, and managing temporary state variables that must persist across different execution phases of a workflow.
Achieving this functionality requires a nuanced understanding of how GitHub Actions handles job lifecycles, the utilization of the GITHUB_STATE file for persistent metadata, and the implementation of custom JavaScript actions to bridge the gap where native composite actions may lack built-in post-step support. By leveraging these mechanisms, developers can ensure that their infrastructure remains clean and their deployment notifications are accurate and timely.
Post-Deployment Automation via Railway Integration
When integrating GitHub Actions with platform-as-a-service providers like Railway, the goal is often to trigger a specific set of actions only after a successful deployment has been confirmed by the external provider. Railway facilitates this by making the deployment status available back to GitHub, which allows the workflow to respond to specific state changes.
The primary mechanism for this is the deployment_status event. This event serves as the trigger for the workflow, allowing the system to monitor when a deployment changes state. Specifically, a post-deploy workflow is designed to target the success state.
To implement this, a workflow file must be created at .github/workflows/post-deploy.yml. This file allows the user to customize the final execution steps.
The operational impact of this configuration is that it separates the deployment process from the notification or verification process. Instead of the deployment script itself attempting to send a webhook—which might fail if the app is still booting—the deployment_status event ensures the webhook is only fired once the platform confirms the app is actually live.
Within this workflow, users can utilize curl or other HTTP clients to send webhooks to external monitoring tools, Slack channels, or Discord servers. The flexibility of this approach is further enhanced by inspecting the github.event object, which provides the necessary metadata to build complex conditional logic, ensuring that post-deployment scripts only run for specific environments or branches.
Advanced Post-Step Implementation in Composite Actions
Composite actions are designed to group multiple steps into a single action. However, a known limitation is the difficulty of implementing "post" steps that run regardless of the success or failure of the main steps within that composite action. To solve this, a specialized Node.js action can be used to indirectly perform these steps.
The with-post-step implementation uses a JavaScript-based action to manage the execution flow. This is achieved by defining an action.yml and a corresponding main.js.
The structure of the action.yml for this utility is as follows:
yaml
name: With post step
description: "Generic JS Action to execute a main command and set a command as a post step."
inputs:
main:
description: "Main command/script."
required: true
post:
description: "Post command/script."
required: true
key:
description: "Name of the state variable used to detect the post step."
required: false
default: POST
runs:
using: "node16"
main: "main.js"
post: "main.js"
The logic within main.js relies on the child_process module to spawn shell commands and the fs module to interact with the GitHub environment. The core logic follows this flow:
- The script checks for an environment variable derived from
INPUT_KEYprefixed withSTATE_. - If
process.env[STATE_${key}]is defined, the script identifies that it is currently in the "post" phase and executes the command provided inINPUT_POST. - If the variable is not defined, the script is in the "main" phase. It then appends a marker to the
GITHUB_STATEfile usingfs.appendFileSyncand executes theINPUT_MAINcommand.
```javascript
const { spawn } = require("child_process");
const { appendFileSync } = require("fs");
const { EOL } = require("os");
function run(cmd) {
const subprocess = spawn(cmd, { stdio: "inherit", shell: true });
subprocess.on("exit", (exitCode) => {
process.exitCode = exitCode;
});
}
const key = process.env.INPUTKEY.toUpperCase();
if ( process.env[STATE_${key}] !== undefined ) {
run(process.env.INPUTPOST);
} else {
appendFileSync(process.env.GITHUBSTATE, ${key}=true${EOL});
run(process.env.INPUTMAIN);
}
```
This pattern allows a composite action (such as a cache action) to utilize the with-post-step utility to ensure that cleanup or logging occurs after the primary cache operation is finished. The directory structure for such an implementation typically follows this hierarchy:
- .github
- actions
- cache
- action.yml
- with-post-step
- action.yml
- main.js
- cache
- workflows
- ci.yml
- actions
Utilizing gacts/run-and-post-run for Workflow Jobs
For users who require a simplified way to execute commands both during and after a job without writing custom JavaScript, the gacts/run-and-post-run@v1 action provides a streamlined interface. This action allows the definition of a "run" command (executed immediately) and a "post" command (executed once the workflow job has ended).
The implementation in a workflow file looks like this:
yaml
jobs:
run-some-action:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run this action
uses: gacts/run-and-post-run@v1
with:
run: echo "First"
post: |
echo "First post"
echo "(can run multiply commands)"
ls -la /tmp /bin /opt
The technical specifications for the inputs used in this action are detailed in the following table:
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
| run | string or list | no | No | A command that needs to be run in place |
| post | string or list | yes | Yes | A command that needs to be run once a workflow job has ended |
| working-directory | string | no | No | A working directory from which the command needs to be run |
| shell | string | bash | no | A shell to use for executing run commands |
| post-shell | string | no | No | A shell to use for executing post commands. Defaults to value of shell |
This tool is particularly useful for tasks that must be guaranteed to run, such as cleaning up temporary directories or uploading logs, regardless of whether the primary "run" command succeeded.
The GITHUB_STATE Mechanism and Metadata Management
The GITHUB_STATE file is a specialized mechanism used by GitHub Actions to pass information from a main action to its corresponding post-action. This is essential for cleanup tasks where the post-action needs to know exactly what was created during the main action.
The GITHUB_STATE file is only accessible within the scope of an action. When a value is written to this file, the runner stores it as an environment variable with the STATE_ prefix for the post-action to consume.
For example, if a main action creates a temporary process and wants to ensure that process is killed during the post-action, it can write the process ID (PID) to the state file using JavaScript:
javascript
import * as fs from 'fs'
import * as os from 'os'
fs.appendFileSync(process.env.GITHUB_STATE, `processID=12345${os.EOL}`, {
encoding: 'utf8'
})
The resulting environment variable STATE_processID is then exclusively available to the cleanup script running under the post-action. The cleanup script can then access this value as follows:
javascript
console.log("The running PID from the main action is: " + process.env.STATE_processID);
There are specific constraints regarding the use of GITHUB_STATE. If multiple pre- or post-actions are utilized within a single workflow, the saved value is only accessible in the specific action where it was written to the state file. This creates a localized scope for state management, preventing conflicts between different actions.
Artifact Management and the upload-artifact Action
In many post-execution scenarios, the primary goal is to preserve the output of the workflow for later analysis. The actions/upload-artifact@v7 action is the standard tool for this purpose, often used in the final steps of a job to upload logs, binaries, or test reports.
The configuration for uploading artifacts involves several key parameters:
name: The name of the artifact. If not provided, it defaults toartifact.path: The required path to the file, directory, or wildcard pattern describing what to upload.if-no-files-found: Determines the behavior when no files match the path. Options includewarn(default),error(fails the action), orignore(no warning or error).retention-days: The duration after which the artifact will expire.
Example usage:
yaml
- uses: actions/upload-artifact@v7
with:
name: deployment-logs
path: logs/
if-no-files-found: warn
Distribution and Release of Custom Post-Run Actions
When developing a custom action like run-and-post-run, the process of releasing the action to the public or a private organization requires a specific build and publish cycle.
The release workflow involves:
- Building the distribution: This is typically done using
make buildornpm run build. - Committing the
distdirectory: It is critical to commit the changes in thedistdirectory to themasterormainbranch, as the GitHub Action runner often looks for the compiled code in this directory. - Tagging and Publishing: A new release must be published via the repository releases page. The git tag must follow the
vX.Y.Zformat (e.g.,v1.0.0).
GitHub automatically manages major and minor tags. For instance, if v1.2.0 is published, the v1 and v1.2 tags are updated automatically, allowing users to reference the action via uses: gacts/run-and-post-run@v1 while always receiving the latest minor updates.
Analysis of Post-Execution Architectures
The necessity of "post" logic in GitHub Actions stems from the inherent volatility of the runner environment. Because runners are ephemeral, any state that needs to persist must be explicitly handled via artifacts or external state files. The transition from simple run steps to complex post actions marks a shift toward more robust DevOps practices.
The use of GITHUB_STATE is a critical architectural choice by GitHub to solve the "cleanup problem." By allowing an action to "remember" its own state, GitHub enables developers to create actions that are self-cleaning, reducing the risk of leaving orphaned processes or temporary files on the runner that could interfere with subsequent jobs in a shared environment.
Furthermore, the integration with third-party platforms like Railway highlights the evolution of Event-Driven Architecture (EDA) in CI/CD. Rather than a linear script that executes Deploy -> Wait -> Notify, the use of the deployment_status event transforms the workflow into a reactive system. This increases reliability, as the notification is triggered by a verified state change from the production environment rather than an assumed success from the deployment tool.
The implementation of "post-steps" in composite actions via Node.js wrappers is a workaround for a current limitation in the GitHub Actions specification. It demonstrates the power of JavaScript actions to manipulate the runtime environment in ways that YAML-based composite actions cannot, specifically by interfacing directly with the GITHUB_STATE file and the child_process module to manage execution flow.