Hudson Dev Workshop
description
Transcript of Hudson Dev Workshop
![Page 2: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/2.jpg)
2
Why a Hudson plugin?
300+ people have done it. Can’t be that hard Integrate Hudson with your favorite tools, systems,
and etc. Make Hudson speak in domain specific terms▪ Instead of batch files or shell scripts
![Page 3: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/3.jpg)
3
Pre-requisite
Maven 2 Terminal and/or command prompt IDE of your choice The following entry in your .m2/settings.xml
Windows user will find this in %USERPROFILE%\.m2\settings.xml
<settings> <pluginGroups> <pluginGroup>org.jvnet.hudson.tools</pluginGroup> </pluginGroups></settings>
![Page 4: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/4.jpg)
4
Create a skeleton
CUI wizard to create a skeleton This will download a lot from the internet if this is
the first time
Download seed.zip and unzip it in ~/.m2/repository for faster bootstrap From my USB key
$ mvn -cpu hpi:create
![Page 5: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/5.jpg)
And while we wait for Maven to download the internet…
![Page 6: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/6.jpg)
6
Hudson Plugin Community
https://github.com/hudson/ Developer resources
IRC channel: #hudson Mailing list: [email protected] Wiki:
http://wiki.hudson-ci.org/display/HUDSON/Extend+Hudson
![Page 7: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/7.jpg)
7
Build your Hudson plugin
Update Hudson version to 1.387 In reference to parent POM
Hudson plugin follows a Maven standard
And while you wait for Maven to build…
$ mvn package$ ls target/*.hpi
![Page 8: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/8.jpg)
8
Let’s look at source files
POM Source files Jelly files Help HTML files
![Page 9: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/9.jpg)
9
Debug a Hudson plugin
Start your plugin in an embedded Hudson If port 8080 is occupied, use –Dport=12345
Point your browser to http://localhost:8080/
$ mvn hpi:run
![Page 10: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/10.jpg)
10
Play with the hello world builder
Configure a new job Add a “Say hello world” build step Run the build and see hello world Where is HUDSON_HOME?
Hint: system configuration
![Page 11: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/11.jpg)
11
Interactive Development
Edit help HTML file, save Just reload the browser and see the change
Edit Jelly files, save Just reload the browser and see the change
![Page 12: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/12.jpg)
12
Interactive Development
For Eclipse Edit source code, save Wait for Hudson to auto reload
For other IDEs Edit source code, compile Wait for Hudson to auto reload
![Page 13: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/13.jpg)
13
Sometimes auto-reload is annoying
Most often useful with “mvnDebug” So that you can reload classes from debugger
Add –DscanIntervalSeconds=0 to Maven
![Page 14: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/14.jpg)
14
When your plugin is ready…
“Manage Hudson” > “Manage Plugins” > “Advancced”
![Page 15: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/15.jpg)
15
Let’s look at the source codemore carefully
![Page 16: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/16.jpg)
16
Key ingredient check list
@DataBoundConstructor @Extension DescriptorImpl Package structure for Jelly files Help files by convention
![Page 17: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/17.jpg)
17
Basic pattern of plugins
Pick an extension point http://wiki.hudson-ci.org/display/HUDSON/
Extension+points Implement it Create a descriptor with @Extension
Or in some cases there’s no descriptor and the implementation class itself gets @Extension
![Page 18: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/18.jpg)
18
UI / Data binding
Getter or public field
@field in config.jelly
Parameter name in
data bound constructor
![Page 19: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/19.jpg)
19
Exercise: add a new config field
Add another text field to accept another name, then say hello to both
Add a help file for this new text field
![Page 20: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/20.jpg)
20
UI Samples
Unfortunately, much of config.jelly editing is monkey-see-monkey-do
UI samples = our attempt to fix this
![Page 21: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/21.jpg)
21
Exercise: add a new config field
Add auto-completion to the name field Note the method name has to match with @field
Extra Credits Try other UI samples and see if you can copy them
![Page 22: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/22.jpg)
22
Form Validation
Write doCheckXyz method for xyz field “value” parameter to receive the current value Can also retrieve nearby sibling values Return/throw FormValidation instance for status
public FormValidation doCheckPort(@QueryParameter String value) {if(looksOk(value)) return FormValidation.ok();else return FormValidation.error("There's a problem here");
}
![Page 23: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/23.jpg)
23
Different datatypes available
URL int boolean Enum hudson.util.Secret
Persisted and sent to browser in encrypted form Can be extended by adding converter
![Page 24: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/24.jpg)
24
Exercise: play with Secret
Add another configuration field whose type is hudson.util.Secret
Inspect the persisted config.xml ./work/jobs/JOBNAME/config.xml
Inspect the value set in the config page Trace the execution in the debugger
![Page 25: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/25.jpg)
25
Playing with Launcher
Abstraction for forking processes Works even when a build is on a slave
Fluent API launcher.launch().doThis().doThat().start()▪ or join()
![Page 26: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/26.jpg)
26
Playing with BuildListener
Used to write to build console getLogger() : PrintStream
Used to report error e.printStackTrace(listener.error(“Failed to do X”));
![Page 27: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/27.jpg)
27
Exercise: Launcher&BuildListener
Tweak HelloWorldBuilder to fork a process and send its output to build console E.g., “uname –a” or “cmd.exe /?”
![Page 28: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/28.jpg)
28
FilePath: working with files
java.io.File on steroid Represents a file or a directory Lots of convenient file operations Works transparently on files on slaves
![Page 29: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/29.jpg)
29
Exercise: FilePath
Tweak HelloWorldBuilder to play with FilePath Use AbstractBuild.getWorkspace()
Create several text files and create a zip file from them
Compute md5 checksums of all the files Hint: getDigest()
![Page 30: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/30.jpg)
30
Descriptor : Describable
Descriptor : Describable = Class : Object Describable
Created from UI through Descriptor Live as long as the configuration remains the same
Descriptor Singletons Capture behaviors and operations that are “static” Global configuration, form validation … is a static nested type of Describable by convention
![Page 31: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/31.jpg)
31
@Extension
Enumerated during compile time to generate index Sometimes “mvn compile” is needed for this Used at runtime to find extensions
![Page 32: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/32.jpg)
View Technology
![Page 33: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/33.jpg)
33
Side track: RootAction
Useful for experimenting with views Extension point to add a menu item to the top
page Descriptor-less, because it’s not configured from
anywhere
![Page 34: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/34.jpg)
34
Apache Commons Jelly
XML based template engine Think of it as JSPX+JSTL+custom taglibs
Used to render HTML And other XMLs, such as RSS, JNLP, …
Most people start by monkey-see-monkey-do References
http://commons.apache.org/jelly/tags.html http://hudson-ci.org/maven-site/hudson-core/
jelly-taglib-ref.html
![Page 35: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/35.jpg)
35
Apache Commons JEXL
EL on steroid ${foo.bar[5]+”abc”} Allow method calls XML friendly operators▪ “and”/”or”,”lt”,”ge” etc in addition to &&, ||, <, >=
a?b:c and a?:b (=a?a:b)
![Page 36: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/36.jpg)
36
Object-oriented views
Views are like methods Co-located to classes via naming convention foo.jelly for org.acme.Bar should be in
org/acme/Bar/foo.jelly Views are inherited to subtypes In JEXL, “it” variable refers to the “this” object “index.jelly” plays the special role
![Page 37: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/37.jpg)
37
Exercise: Jelly, JEXL, and RootAction
Create a new root action “MyRootAction” See javadoc for what your methods need to return
Add index.jelly Start from this:
Play with Jelly and JEXL Add methods to your class and invoke it from view Use <j:forEach> to say hello 10 times Otherwise be creative!
<j:jelly xmlns:j=“jelly:core”> <html><body><h1> Hello from ${it.getClass()} </h1></body></html></j:jelly>
![Page 38: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/38.jpg)
38
Object-oriented Views
In Hudson, URLs are mapped to object graph And this determines how request is eventually
handled IOW, controller layer is implicit More precisely, this is in Stapler
See HTTP headers for evaluation trace So if you add “bar.jelly” to MyRootAction,
![Page 39: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/39.jpg)
39
Exercise: Object-oriented views
Add “bar.jelly” to MyRootAction Modify index.jelly to add a hyperlink to the
new page Refactor MyRootAction and introduce a base
type Push down bar.jelly to the new base class. See
how the UI behaves
![Page 40: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/40.jpg)
40
URL → Object graph mapping
![Page 41: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/41.jpg)
41
URL → Object graph mapping
Typical traversal methods getFoo() → …/foo/ getFoo(“bar”) → …/foo/bar/ getDynamic(“foo”) → …/foo/…
Exact rules are in https://stapler.dev.java.net/reference.html
![Page 42: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/42.jpg)
42
Exercise: URL → Object graph
Define immutable Employee class And populate several of them from MyRootAction
Define index.jelly on Employee and create navigable UI
public class Employee { public int number; public String name; public Employee boss; public List<Employee> reports; …}
public class MyRootAction { List<Employee> employees;}
![Page 43: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/43.jpg)
43
Programmatic request handling
Instead of “foo.jelly”, define “doFoo” method Parameters are injected
StaplerRequest (<: HttpServletRequest) StaplerResponse (<: HttpServletResponse) @QueryParameter @AncestorInPath
Return value / exception becomes HTTP response (or void) Or void and control it yourself org.kohsuke.stapler.HttpResponse
![Page 44: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/44.jpg)
44
Demo
Employee.doTerminate()
![Page 45: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/45.jpg)
45
Exercise: doXyz method
Define a form that takes one text field and update the employee name
![Page 46: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/46.jpg)
Persistence
![Page 47: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/47.jpg)
47
XStream
This is how Hudson persists most data Characteristics
Clean almost human-readable XML No mapping required / semi-transparent to code Supports arbitrary object graph Customizable enough Robust in the face of partial unmarshalling failure
![Page 48: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/48.jpg)
48
XStream: naïve example
public class Employee { public int number; public String name;}
<org.acme.Employee><number>1</number><name>Kohskue</name>
</org.acme.Employee>
![Page 49: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/49.jpg)
49
Persistence and extension points
If/how your extensions are persisted is described in javadoc E.g., Builder is persisted as a part of
AbstractProject SecurityRealm is persisted as a part of Hudson etc.
![Page 50: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/50.jpg)
50
XmlFile
Used if you need to persist things on your own
Represents a XML file that contains persisted objects
Important methods write(Object) Object read() void unmarshal(Object)
![Page 51: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/51.jpg)
51
Data format evolution
Fields can be added to class Field is left to VM default value when reading old data
Fields can be deleted from class Extra data is thrown away when reading old data
Fields can change type But only if the XML remains compatible
Transient fields are read but not written Convenient for migration that involves computation
(in conjunction with readObject)
![Page 52: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/52.jpg)
52
Exercise: persistence
Creates MyRootAction.doTest method And use this method to experiment with XmlFile
Test data format evolution Define a class that has a boolean field Save a file Evolve this boolean to 3-state enum Define a data migration logic▪ Must be able to read old data▪ But write 3-stete enum, not boolean
![Page 53: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/53.jpg)
53
Exercise: persistence
Add a new boolean config option to HelloWorldBuilder And it should default to true
![Page 54: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/54.jpg)
Remoting
![Page 55: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/55.jpg)
55
Remoting overview
How does Hudson execute code on slaves, when slaves only has 17KB slave.jar?Q
![Page 56: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/56.jpg)
56
The answer
By sending class files and loading them into classloaders on-demand
Master serializes a Callable, slave execute that and the result is send back to master
All in all, not so far away from what RMI did
![Page 57: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/57.jpg)
57
Basics
A pair of Channels in 2 JVMs Connected by Input/OutputStream pair
Not sure how to get to Channel? Computer.getChannel() Computer.currentComputer()
JVM1 JVM2
Channel Channel
![Page 58: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/58.jpg)
58
Careful!
Serialization sucks in everything referencible Convenient but can be dangerous Beware of anonymous Callable▪ static is your friend
Don’t mask InterruptedException
![Page 59: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/59.jpg)
59
Exercise: remoting basics
Connect a few slaves Define a few slaves with JNLP launcher Launch them from your own computer▪ You now have several VMs (but on the same machine)
Use Channel.call and retrieve RuntimeMXBean.getName()
![Page 60: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/60.jpg)
60
More ways to pass data
Pipe Mechanism for creating stream between local and
remote
RemoteInputStream/RemoteOutputStream Not symmetric in performance
![Page 61: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/61.jpg)
61
Moving computation vs moving data
Execute on master Data is remote Easier to write Mostly transparent
Thanks to FilePath, Launcher, etc.
Execute on slave Data is local Scales better Performs better Explicit use of remoting
Start from left and shift to rightas you gain more experience
![Page 62: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/62.jpg)
Unit Testing
![Page 63: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/63.jpg)
63
Introducing HudsonTestCase
Our attempt at making testing easier JUnit extension Also a great sandbox environment to
experiment
![Page 64: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/64.jpg)
64
Embedded Jetty
Hudson is started inside the JVM with Jetty Random available port for HTTP Create a real deployed environment Number of annotations to control initial state
Integrated HtmlUnit to emulate browser access Bunch of convenience methods
![Page 65: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/65.jpg)
65
In-memory access
All Hudson objects accessible directly Drastically simplifies test set up and side-effect
verifications A number of convenience methods
![Page 66: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/66.jpg)
66
Demo
Create/setup a project Perform a build Check the result Create a slave Use HtmlUnit Interactive Break JavaScript Debugger
![Page 67: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/67.jpg)
67
Exercise: write unit tests
Makes sure that HelloWorldBuilder did a proper greeting
Hints HudsonTestCase.createFreeStyleProject() FreeStyleProject.getBuildersList().add() HudsonTestCase.assertBuildStatusSuccess() FreeStyleBuild.getLog()
![Page 68: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/68.jpg)
68
Exercise: write unit tests
Make sure MyRootAction shows in UI Hints
HudsonTestCase.createWebClient()
![Page 69: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/69.jpg)
Internationalization
![Page 70: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/70.jpg)
70
i18n Basics
i18n in Jelly files i18n in Java source files i18n in help HTML files
![Page 71: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/71.jpg)
71
i18n in Jelly
${%...} notation for default English locale
Translations
Glued together by naming convention foo.jelly and foo_ja.properties, side-by-side
<h1>${%Output from Maven}</h1>
Output\ from\ Maven=Mavenからの出力
![Page 72: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/72.jpg)
72
i18n in Jelly with parameters
${%...(…)}
Translations in MessageFormat syntax
<h1>${%output(it.name)}</h1>
foo.properties:output=Output from {0}
foo_ja.properties:Output={0}からの出力
![Page 73: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/73.jpg)
73
i18n in Java source code
Hudson expects one Messages.properties per package Build generates type-safe Messages class to use
resources
![Page 74: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/74.jpg)
74
i18n in help files
Glued together by naming convention help-foo_ja.html for help-foo.html
Ditto for Jelly-based help files
![Page 75: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/75.jpg)
Phew
![Page 76: Hudson Dev Workshop](https://reader035.fdocuments.us/reader035/viewer/2022062410/56816648550346895dd9c087/html5/thumbnails/76.jpg)
76
Where to go from here?
Keep in touch with us at [email protected] and IRC #hudson
Look for plugins that does something similar, use it as the basis
Host the plugins with the Hudson community