Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm
Expand All @@ -24,3 +24,24 @@ jobs:

- name: Lint and test
run: npm run check

commitlint:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm

- name: Install dependencies
run: npm ci

- name: Lint commit messages
run: npm run lint:commits
53 changes: 53 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Release

on:
push:
branches:
- main

permissions:
contents: write
issues: write
pull-requests: write

concurrency:
group: release
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 24

- name: Release
id: release
uses: cycjimmy/semantic-release-action@v6
with:
semantic_version: 25
extra_plugins: |
@semantic-release/changelog@7
@semantic-release/git@11
conventional-changelog-conventionalcommits@9
env:
# A token with bypass rights on the main ruleset; the release
# commit is pushed straight to main.
GITHUB_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }}

- name: Move floating major tag
if: steps.release.outputs.new_release_published == 'true'
env:
MAJOR_TAG: v${{ steps.release.outputs.new_release_major_version }}
RELEASE_TAG: ${{ steps.release.outputs.new_release_git_tag }}
run: |
set -euo pipefail
git tag -f "$MAJOR_TAG" "$RELEASE_TAG"
git push -f origin "$MAJOR_TAG"
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
24
17 changes: 17 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"branches": ["main"],
"plugins": [
["@semantic-release/commit-analyzer", { "preset": "conventionalcommits" }],
[
"@semantic-release/release-notes-generator",
{ "preset": "conventionalcommits" }
],
"@semantic-release/changelog",
["@semantic-release/npm", { "npmPublish": false }],
[
"@semantic-release/git",
{ "assets": ["CHANGELOG.md", "package.json", "package-lock.json"] }
],
"@semantic-release/github"
]
}
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Reload A/S

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
190 changes: 104 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,126 +1,144 @@
# Upsun Github Environment Synchonization
# Upsun GitHub Environment Sync

This document describes how to set up the GitHub deployment and environment
synchronization activity script on Upsun.
An Upsun activity script that syncs Upsun environments to GitHub deployments, and a GitHub Action that installs it in a project.

## Overview
## Contents

The activity script in `activity-script.js` automatically synchronizes Upsun
environment states with GitHub Deployments. It responds to environment lifecycle
events and creates/updates GitHub deployment statuses accordingly.
- `activity-script.js`: The activity script. Upsun runs it on environment events.
- `action.yml` and `setup.js`: The composite GitHub Action that installs or updates the script in a project.
- `examples/upsun-github-env-sync.yml`: A workflow file for consumer repositories.

### Event handling
## How it works

| Upsun Event | Activity State | GitHub Status | Details |
|-----------------------------|----------------|---------------|--------------------------|
| `environment.push` | pending | queued | Deployment queued |
| `environment.push` | in_progress | in_progress | Links to deployment log |
| `environment.push` | complete (success) | success | Links to environment url |
| `environment.push` | complete (failure) | failure | Links to deployment log |
| `environment.activate` | complete | success | Environment activated |
| `environment.domain.create` | complete | success | Environment url updated |
| `environment.domain.delete` | complete | success | Environment url updated |
| `environment.deactivate` | complete | inactive | Environment closed |
| `environment.delete` | complete | inactive | Environment deleted |
Upsun runs the activity script on these events. The script maps each event and activity state to a GitHub deployment status:

### Deployment creation
| Upsun event | Activity state | GitHub status | Details |
| --------------------------- | ------------------ | ------------- | ------------------------------- |
| `environment.push` | pending | queued | New deployment for the activity |
| `environment.push` | in_progress | in_progress | Links to the Upsun log |
| `environment.push` | complete (success) | success | Links to the environment URL |
| `environment.push` | complete (failure) | failure | Links to the Upsun log |
| `environment.redeploy` | pending | queued | Updates the latest deployment |
| `environment.redeploy` | in_progress | in_progress | Links to the Upsun log |
| `environment.redeploy` | complete (success) | success | Links to the environment URL |
| `environment.redeploy` | complete (failure) | failure | Links to the Upsun log |
| `environment.activate` | complete | success | Environment activated |
| `environment.domain.create` | complete | success | Environment URL updated |
| `environment.domain.delete` | complete | success | Environment URL updated |
| `environment.deactivate` | complete | inactive | Environment closed |
| `environment.delete` | complete | inactive | Environment deleted |

The script automatically creates or updates GitHub deployments when:
- Code is pushed to an environment
- An environment is activated for the first time
- A domain is added or removed for the environment
A push creates one GitHub deployment per Upsun activity. All other events update the latest deployment of the environment.

The script will:
1. Check if a deployment exists for the environment
2. Create one if it doesn't exist
3. Update the deployment status based on activity state
The action connects to the Upsun project with the Upsun CLI. It creates a `script` integration with the events above, or updates the one it created before. Then it sets three variables on the integration:

### URLs generated
- `GH_TOKEN` (sensitive): The GitHub token that the script uses.
- `GH_REPO`: The repository, taken from the workflow that runs the action.
- `UPSUN_GITHUB_ENV_SYNC_VERSION`: The installed release of this repository.

* Environment url: Extracted from the Upsun primary route
* Log url: Use the format `https://console.upsun.com/{OWNER_SLUG}/{PROJECT_ID}/-/log/{ACTIVITY_ID}`
## Install the script in a project

## Prerequisites
Do these steps once for each consumer repository.

1. **GitHub Personal Access Token**
- Create a token at: https://github.com/settings/personal-access-tokens/new
- Repository access: Select the repository for the project
- Permissions:
- Metadata: Read only (default)
- Deployments: Read and write
- Environments: Read and write
- Store the token securely
1. Create a fine-grained personal access token. Use a bot account if your organization has one, so the token does not stop when a person leaves. Give it access to the consumer repository only. Give it the longest lifetime that the organization policy permits. Give it these repository permissions:
- `Metadata`: Read-only
- `Contents`: Read-only
- `Pull requests`: Read-only
- `Deployments`: Read and write
- `Environments`: Read and write
2. In the consumer repository, create the repository variable `UPSUN_PROJECT_ID` with the Upsun project ID.
3. In the consumer repository, create the repository secret `GH_DEPLOY_TOKEN` with the token from step 1.
4. Make sure that the repository can read a secret `UPSUN_API_TOKEN` that holds an Upsun API token with access to the project. Create the token in the Upsun Console, under the account settings of a user or a dedicated API user. See [Upsun API tokens](https://developer.upsun.com/cli/api-tokens) for the steps in the Console. A GitHub organization secret lets all consumer repositories share one token. If the secret is an organization secret, ask an organization owner to give the repository access.
5. Copy `examples/upsun-github-env-sync.yml` to `.github/workflows/` in the consumer repository. If the default branch is not `main`, change the branch name in the file.
6. Merge the workflow file. The push installs the script.
7. Open the workflow run. Make sure that the last step ends with `Synchronized integration` and an ID.

2. **Upsun CLI**
- Install: `curl -fsSL https://raw.githubusercontent.com/platformsh/cli/main/installer.sh | bash`
- Login: `upsun login`
The workflow also runs every night at 04:00 UTC. This run installs new releases of the script.

## Development and validation
### Inputs

### 1. Install the activity script
| Input | Required | Description |
| --------------------- | -------- | -------------------------------------------------------------------------- |
| `upsun_project_id` | Yes | The Upsun project ID. |
| `upsun_api_token` | Yes | An Upsun API token with access to the project. |
| `github_deploy_token` | Yes | The GitHub token that the script uses. See step 1 for permissions. |
| `upsun_cli_version` | No | The Upsun CLI version to install, for example `5.11.0`. Default is latest. |

```bash
upsun integration:add \
--type script \
--file .platform/activity-scripts/upsun-github-env-sync/activity-script.js \
--events='environment.push,environment.activate,environment.domain.create,environment.domain.delete,environment.deactivate,environment.delete' \
--states='*' \
--environments='*'
### Outputs

| Output | Description |
| ---------------- | -------------------------------------------------- |
| `integration_id` | The ID of the integration that the action manages. |

## Versions

Each release gets a tag, for example `v1.2.0`. The tag `v1` always points to the latest release in the `v1` line.

The example workflow uses `reload/upsun-github-env-sync@v1`. The nightly run then installs each new release without a change in the consumer repository. To stop on one release, use the exact tag instead:

```yaml
uses: reload/upsun-github-env-sync@v1.2.0
```

### 2. Set required integration variables
## Troubleshooting

### Using the Upsun CLI

The commands in this section use the Upsun CLI on your computer. To install it, run the installer from [upsun/cli](https://github.com/upsun/cli). Then run `upsun login`. To list the integrations of a project and their IDs:

```bash
# Set GitHub token (required)
upsun api:curl /api/projects/[PROJECT_ID]/integrations/[INTEGRATION_ID]/variables -X POST --json="{
\"name\": \"GH_TOKEN\",
\"value\": \"[GITHUB_TOKEN]\",
\"is_sensitive\": true
}"

# Set GitHub repository (required - format: owner/repo, no [])
upsun api:curl /api/projects/[PROJECT_ID]/integrations/[INTEGRATION_ID]/variables -X POST --json="{
\"name\": \"GH_REPO\",
\"value\": \"[GITHUB OWNER]/[GITHUB REPOSITORY]\"
}"
upsun integrations --project PROJECT_ID
```

## Debugging
### Which integration the action manages

### View activity script logs
The action manages a script integration only if it has the variable `UPSUN_GITHUB_ENV_SYNC_VERSION`. The action creates this variable on the integration that it creates. The action does not read or change other integrations.

```bash
# View recent activity script executions
upsun integration:activities INTEGRATION_ID
If the project has no integration with the variable, the action creates one. If the project has one, the action updates it. If the project has more than one, the action stops with `Found 2 script integrations`. Delete the extra integrations and run the workflow again:

# View logs for a specific activity
upsun integration:activity:log INTEGRATION_ID ACTIVITY_ID
```bash
upsun integration:delete INTEGRATION_ID --project PROJECT_ID
```

## Maintenance
### Replacing an integration installed by hand

### Update the Script
The action does not adopt an integration that you installed by hand. To replace it, delete it with the command above and run the workflow.

```bash
# After editing activity-script.js
### GitHub deployments stop updating

The token in `GH_DEPLOY_TOKEN` has probably expired. The workflow does not find this error, because the workflow does not call GitHub with the token. Create a new token, update the secret, and run the workflow by hand.

# 1. Validate the changes
npm run activity-script:lint
### Reading the log of the script

# 2. Update the integration
upsun integration:update \
--file .platform/activity-scripts/upsun-github-env-sync/activity-script.js \
INTEGRATION_ID
```bash
upsun integration:activities INTEGRATION_ID --project PROJECT_ID
upsun integration:activity:log INTEGRATION_ID ACTIVITY_ID --project PROJECT_ID
```

### Disable the integration
## Development

The Node version is in `.nvmrc`.

```bash
upsun integration:delete INTEGRATION_ID
nvm use
npm ci
npm run check
```

`npm run check` runs Prettier, the TypeScript check, and the tests. `npm run format` corrects the formatting.

## Release

Commit messages must follow [Conventional Commits](https://www.conventionalcommits.org/). CI lints every commit in a pull request. To lint your commits before you push, run `npm run lint:commits`.

On each push to `main`, semantic-release reads the new commits. A `fix` commit makes a patch release. A `feat` commit makes a minor release. A commit with `!` after the type, or a `BREAKING CHANGE` footer, makes a major release. Other types make no release.

A release creates a Git tag and a GitHub release, updates `CHANGELOG.md` and `package.json`, and moves the major tag. The package is not published to npm or another registry. Consumers use the action from the Git tag.

The release commit is pushed to `main` with the secret `GH_RELEASE_TOKEN`. It holds a personal access token with `Contents: Read and write` on this repository, from an account in the bypass list of the ruleset on `main`.

## References

- [Upsun Activity Scripts Documentation](https://docs.upsun.com/integrations/activity.html)
- [Upsun Activity Reference](https://docs.upsun.com/integrations/activity/reference.html)
- [Upsun activity scripts](https://developer.upsun.com/docs/integrations/activity)
- [Upsun activity reference](https://developer.upsun.com/docs/integrations/activity/reference)
- [GitHub Deployments API](https://docs.github.com/en/rest/deployments/deployments)
Loading