1+ name : Reusable NPM Release Workflow
2+
3+ on :
4+ workflow_call :
5+ inputs :
6+ repository :
7+ description : ' Target repository (e.g., GoPlasmatic/Sandbox)'
8+ required : true
9+ type : string
10+ working-directory :
11+ description : ' Working directory for npm commands'
12+ required : false
13+ type : string
14+ default : ' website'
15+ node-version :
16+ description : ' Node.js version to use'
17+ required : false
18+ type : string
19+ default : ' 20'
20+ dry-run :
21+ description : ' Dry run (skip release)'
22+ required : false
23+ type : boolean
24+ default : false
25+ docker-enabled :
26+ description : ' Enable Docker image build and push'
27+ required : false
28+ type : boolean
29+ default : false
30+ docker-image-name :
31+ description : ' Docker image name (required if docker-enabled)'
32+ required : false
33+ type : string
34+ secrets :
35+ GH_PAT :
36+ required : true
37+ ACR_URL :
38+ required : false
39+ ACR_USERNAME :
40+ required : false
41+ ACR_PASSWORD :
42+ required : false
43+
44+ jobs :
45+ release :
46+ runs-on : ubuntu-latest
47+ steps :
48+ - name : Checkout target repository
49+ uses : actions/checkout@v4
50+ with :
51+ repository : ${{ inputs.repository }}
52+ token : ${{ secrets.GH_PAT }}
53+ fetch-depth : 0
54+
55+ - name : Setup Node.js
56+ uses : actions/setup-node@v4
57+ with :
58+ node-version : ${{ inputs.node-version }}
59+ cache : ' npm'
60+ cache-dependency-path : ${{ inputs.working-directory }}/package-lock.json
61+
62+ - name : Install dependencies
63+ working-directory : ${{ inputs.working-directory }}
64+ run : npm ci
65+
66+ - name : Run tests
67+ working-directory : ${{ inputs.working-directory }}
68+ run : npm test --if-present
69+
70+ - name : Build application
71+ working-directory : ${{ inputs.working-directory }}
72+ run : npm run build
73+
74+ - name : Get version from package.json
75+ id : get_version
76+ working-directory : ${{ inputs.working-directory }}
77+ run : |
78+ VERSION=$(node -p "require('./package.json').version")
79+ echo "version=$VERSION" >> $GITHUB_OUTPUT
80+ echo "Version: $VERSION"
81+
82+ - name : Check if tag exists
83+ id : check_tag
84+ run : |
85+ if git rev-parse "v${{ steps.get_version.outputs.version }}" >/dev/null 2>&1; then
86+ echo "exists=true" >> $GITHUB_OUTPUT
87+ echo "Tag v${{ steps.get_version.outputs.version }} already exists"
88+ else
89+ echo "exists=false" >> $GITHUB_OUTPUT
90+ echo "Tag v${{ steps.get_version.outputs.version }} does not exist"
91+ fi
92+
93+ - name : Fail if tag exists
94+ if : steps.check_tag.outputs.exists == 'true'
95+ run : |
96+ echo "Error: Version ${{ steps.get_version.outputs.version }} already exists as a tag"
97+ echo "Please update the version in package.json before releasing"
98+ exit 1
99+
100+ - name : Create Dockerfile if not exists
101+ if : inputs.docker-enabled && !inputs.dry-run
102+ run : |
103+ if [ ! -f Dockerfile ]; then
104+ cat > Dockerfile << 'EOF'
105+ # Build stage
106+ FROM node:${{ inputs.node-version }}-alpine AS builder
107+
108+ WORKDIR /app
109+
110+ # Copy package files
111+ COPY ${{ inputs.working-directory }}/package*.json ./
112+
113+ # Install dependencies
114+ RUN npm ci --only=production
115+
116+ # Copy application files
117+ COPY ${{ inputs.working-directory }}/ ./
118+
119+ # Build the application
120+ RUN npm run build
121+
122+ # Runtime stage
123+ FROM node:${{ inputs.node-version }}-alpine
124+
125+ # Install dumb-init for proper signal handling
126+ RUN apk add --no-cache dumb-init
127+
128+ # Create non-root user
129+ RUN addgroup -g 1001 -S nodejs && \
130+ adduser -S nodejs -u 1001
131+
132+ WORKDIR /app
133+
134+ # Copy built application from builder
135+ COPY --from=builder --chown=nodejs:nodejs /app ./
136+
137+ # Switch to non-root user
138+ USER nodejs
139+
140+ # Expose port (adjust as needed)
141+ EXPOSE 3000
142+
143+ # Use dumb-init to handle signals properly
144+ ENTRYPOINT ["dumb-init", "--"]
145+
146+ # Start the application
147+ CMD ["npm", "start"]
148+ EOF
149+ echo "Created Dockerfile"
150+ else
151+ echo "Dockerfile already exists"
152+ fi
153+
154+ - name : Build Docker image
155+ if : inputs.docker-enabled && !inputs.dry-run
156+ run : |
157+ docker build -t ${{ inputs.docker-image-name }}:${{ steps.get_version.outputs.version }} .
158+ docker tag ${{ inputs.docker-image-name }}:${{ steps.get_version.outputs.version }} ${{ inputs.docker-image-name }}:latest
159+
160+ - name : Log in to Azure Container Registry
161+ if : inputs.docker-enabled && !inputs.dry-run
162+ uses : azure/docker-login@v1
163+ with :
164+ login-server : ${{ secrets.ACR_URL }}
165+ username : ${{ secrets.ACR_USERNAME }}
166+ password : ${{ secrets.ACR_PASSWORD }}
167+
168+ - name : Push Docker image to ACR
169+ if : inputs.docker-enabled && !inputs.dry-run
170+ run : |
171+ docker tag ${{ inputs.docker-image-name }}:${{ steps.get_version.outputs.version }} ${{ secrets.ACR_URL }}/${{ inputs.docker-image-name }}:${{ steps.get_version.outputs.version }}
172+ docker tag ${{ inputs.docker-image-name }}:latest ${{ secrets.ACR_URL }}/${{ inputs.docker-image-name }}:latest
173+ docker push ${{ secrets.ACR_URL }}/${{ inputs.docker-image-name }}:${{ steps.get_version.outputs.version }}
174+ docker push ${{ secrets.ACR_URL }}/${{ inputs.docker-image-name }}:latest
175+
176+ - name : Create and push tag
177+ if : ' !inputs.dry-run'
178+ run : |
179+ git config user.name github-actions
180+ git config user.email github-actions@github.com
181+ git tag -a "v${{ steps.get_version.outputs.version }}" -m "Release version ${{ steps.get_version.outputs.version }}"
182+ git push origin "v${{ steps.get_version.outputs.version }}"
183+
184+ - name : Create GitHub Release
185+ if : ' !inputs.dry-run'
186+ uses : actions/create-release@v1
187+ env :
188+ GITHUB_TOKEN : ${{ secrets.GH_PAT }}
189+ with :
190+ tag_name : v${{ steps.get_version.outputs.version }}
191+ release_name : Release v${{ steps.get_version.outputs.version }}
192+ body : |
193+ ## Release v${{ steps.get_version.outputs.version }}
194+
195+ ### Changes
196+ - See commit history for changes
197+
198+ ### Docker Image
199+ ${{ inputs.docker-enabled && format('Docker image available at: `{0}/{1}:{2}`', secrets.ACR_URL, inputs.docker-image-name, steps.get_version.outputs.version) || 'No Docker image for this release' }}
200+ draft : false
201+ prerelease : false
202+
203+ - name : Summary
204+ run : |
205+ echo "## Release Summary" >> $GITHUB_STEP_SUMMARY
206+ echo "" >> $GITHUB_STEP_SUMMARY
207+ if [ "${{ inputs.dry-run }}" == "true" ]; then
208+ echo "**DRY RUN MODE** - No actual release was created" >> $GITHUB_STEP_SUMMARY
209+ else
210+ echo "✅ Successfully released version ${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
211+ echo "" >> $GITHUB_STEP_SUMMARY
212+ echo "- Repository: ${{ inputs.repository }}" >> $GITHUB_STEP_SUMMARY
213+ echo "- Tag: v${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
214+ if [ "${{ inputs.docker-enabled }}" == "true" ]; then
215+ echo "- Docker Image: ${{ secrets.ACR_URL }}/${{ inputs.docker-image-name }}:${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
216+ fi
217+ fi
0 commit comments